From 20b79f1be7dc7cb2352dfa875ea20c0cb7f7f93a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:21:56 +0000 Subject: [PATCH 01/10] Compact MCP CLI help to fit first 20 lines Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 77 +++++++++++++++++------- actions/setup/js/mcp_cli_bridge.test.cjs | 38 +++++++++++- 2 files changed, 93 insertions(+), 22 deletions(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index 0ec0fc6826a..56c2393ec29 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -54,6 +54,9 @@ const KEEPALIVE_PING_INTERVAL_MS = 10000; /** Starting JSON-RPC ID for keepalive ping requests */ const KEEPALIVE_PING_ID_START = 1000; +/** Preferred max lines for generated CLI help output */ +const HELP_MAX_LINES = 20; + // --------------------------------------------------------------------------- // Audit logging // --------------------------------------------------------------------------- @@ -763,20 +766,27 @@ function loadTools(toolsFile) { * @param {Array<{name: string, description?: string}>} tools - Tool list */ function showHelp(serverName, tools) { - const lines = [`Usage: ${serverName} [options]`, ""]; - lines.push("Available commands:"); + const lines = [ + `Usage: ${serverName} [--param value ...]`, + `Tip: ${serverName} --help`, + "", + `Commands (${tools.length}):`, + ]; if (tools.length > 0) { - // Calculate column width for aligned output - const maxNameLen = Math.max(...tools.map(t => t.name.length)); - for (const tool of tools) { - const padded = tool.name.padEnd(maxNameLen + 2); - lines.push(` ${padded}${tool.description || "No description"}`); + const reservedLines = 3; // blank + footer + potential overflow marker + const maxCommandLines = Math.max(1, HELP_MAX_LINES - lines.length - reservedLines); + const shownTools = tools.slice(0, maxCommandLines); + for (const tool of shownTools) { + lines.push(` ${tool.name} — ${summarizeHelpText(tool.description || "No description", 68)}`); + } + if (tools.length > shownTools.length) { + lines.push(` ... +${tools.length - shownTools.length} more command(s)`); } } else { lines.push(" (tool list unavailable)"); } lines.push(""); - lines.push(`Run '${serverName} --help' for more information on a command.`); + lines.push(`Run '${serverName} --help' for option details.`); process.stdout.write(lines.join("\n") + "\n"); } @@ -796,26 +806,32 @@ function showToolHelp(serverName, toolName, tools) { return; } - const lines = [`Command: ${toolName}`, `Description: ${tool.description || "No description"}`]; + const lines = [ + `Command: ${toolName}`, + `Description: ${summarizeHelpText(tool.description || "No description", 90)}`, + `Usage: ${serverName} ${toolName} [--param value ...]`, + `JSON mode: printf '{"param":"value",...}' | ${serverName} ${toolName} .`, + ]; const props = tool.inputSchema?.properties; const required = new Set(tool.inputSchema?.required || []); if (props && Object.keys(props).length > 0) { lines.push(""); - lines.push("Options:"); - const maxKeyLen = Math.max(...Object.keys(props).map(k => k.length)); - for (const [key, val] of Object.entries(props)) { - const flagPad = `--${key}`.padEnd(maxKeyLen + 4); - const parts = [getTypeStr(val.type)]; - if (required.has(key)) parts.push("(required)"); - if (val.description) parts.push(val.description); - lines.push(` ${flagPad}${parts.join(" ")}`); + lines.push(`Options (${Object.keys(props).length}):`); + const optionEntries = Object.entries(props); + const reservedLines = 2; // footer + potential overflow marker + const maxOptionLines = Math.max(1, HELP_MAX_LINES - lines.length - reservedLines); + const shownOptions = optionEntries.slice(0, maxOptionLines); + for (const [key, val] of shownOptions) { + const requiredMark = required.has(key) ? "*" : ""; + const description = val.description ? ` - ${summarizeHelpText(val.description, 62)}` : ""; + lines.push(` --${key} ${getTypeStr(val.type)}${requiredMark}${description}`); + } + if (optionEntries.length > shownOptions.length) { + lines.push(` ... +${optionEntries.length - shownOptions.length} more option(s)`); } - - lines.push(""); - lines.push(`Usage: ${serverName} ${toolName} [--param value ...]`); - lines.push(` or: printf '{"param":"value",...}' | ${serverName} ${toolName} .`); } + lines.push("Required options are marked with *."); process.stdout.write(lines.join("\n") + "\n"); } @@ -832,6 +848,23 @@ function getTypeStr(type) { return `(${types.length > 0 ? types.join("|") : "null"})`; } +/** + * Collapse whitespace and trim long help text for compact output. + * + * @param {string} value + * @param {number} maxLen + * @returns {string} + */ +function summarizeHelpText(value, maxLen) { + const normalized = String(value || "") + .replace(/\s+/g, " ") + .trim(); + if (normalized.length <= maxLen) { + return normalized; + } + return `${normalized.slice(0, Math.max(0, maxLen - 1))}…`; +} + // --------------------------------------------------------------------------- // Response formatting // --------------------------------------------------------------------------- @@ -1130,6 +1163,8 @@ module.exports = { extractJSONRPCMessages, renderProgressMessages, formatResponse, + showHelp, + showToolHelp, hasStdinJsonPayload, readStdinSync, main, diff --git a/actions/setup/js/mcp_cli_bridge.test.cjs b/actions/setup/js/mcp_cli_bridge.test.cjs index 89476c12846..73808e0e59d 100644 --- a/actions/setup/js/mcp_cli_bridge.test.cjs +++ b/actions/setup/js/mcp_cli_bridge.test.cjs @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { formatResponse, hasStdinJsonPayload, parseToolArgs, readStdinSync } from "./mcp_cli_bridge.cjs"; +import { formatResponse, hasStdinJsonPayload, parseToolArgs, readStdinSync, showHelp, showToolHelp } from "./mcp_cli_bridge.cjs"; describe("mcp_cli_bridge.cjs", () => { let originalCore; @@ -212,6 +212,42 @@ describe("mcp_cli_bridge.cjs", () => { expect(process.exitCode).toBe(0); }); + it("keeps top-level help compact for many commands", () => { + const tools = Array.from({ length: 25 }, (_, i) => ({ + name: `tool_${i + 1}`, + description: `Description for command ${i + 1} that is intentionally verbose for truncation checks.`, + })); + + showHelp("safeoutputs", tools); + + const outputLines = stdoutChunks.join("").trimEnd().split("\n"); + expect(outputLines.length).toBeLessThanOrEqual(20); + expect(outputLines.join("\n")).toContain("... +"); + }); + + it("keeps command help compact for many options", () => { + const properties = {}; + for (let i = 1; i <= 24; i++) { + properties[`field_${i}`] = { type: "string", description: `Field ${i} description with additional details for truncation.` }; + } + + showToolHelp("safeoutputs", "create_issue", [ + { + name: "create_issue", + description: "Create an issue with many available fields and optional metadata.", + inputSchema: { + properties, + required: ["field_1", "field_2"], + }, + }, + ]); + + const outputLines = stdoutChunks.join("").trimEnd().split("\n"); + expect(outputLines.length).toBeLessThanOrEqual(20); + expect(outputLines.join("\n")).toContain("... +"); + expect(outputLines.join("\n")).toContain("Required options are marked with *."); + }); + describe("stdin placeholder removed — '-' is always a literal value", () => { it("passes '--key -' as literal '-' (space-separated form)", () => { const schemaProperties = { body: { type: "string" } }; From a634c231c63b625f7a2188c9e070ee4758228e46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:22:48 +0000 Subject: [PATCH 02/10] Show required-marker note only when options exist Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index 56c2393ec29..fcb67c0ac90 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -830,8 +830,8 @@ function showToolHelp(serverName, toolName, tools) { if (optionEntries.length > shownOptions.length) { lines.push(` ... +${optionEntries.length - shownOptions.length} more option(s)`); } + lines.push("Required options are marked with *."); } - lines.push("Required options are marked with *."); process.stdout.write(lines.join("\n") + "\n"); } From 064789dd401b47ca8b7925c02c1bbdd6dca6d24a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:23:37 +0000 Subject: [PATCH 03/10] Harden help truncation and overflow assertions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 8 +++++++- actions/setup/js/mcp_cli_bridge.test.cjs | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index fcb67c0ac90..ccda70274ea 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -856,13 +856,19 @@ function getTypeStr(type) { * @returns {string} */ function summarizeHelpText(value, maxLen) { + if (!Number.isFinite(maxLen) || maxLen <= 0) { + return ""; + } const normalized = String(value || "") .replace(/\s+/g, " ") .trim(); if (normalized.length <= maxLen) { return normalized; } - return `${normalized.slice(0, Math.max(0, maxLen - 1))}…`; + if (maxLen === 1) { + return "…"; + } + return `${normalized.slice(0, maxLen - 1)}…`; } // --------------------------------------------------------------------------- diff --git a/actions/setup/js/mcp_cli_bridge.test.cjs b/actions/setup/js/mcp_cli_bridge.test.cjs index 73808e0e59d..7319d3a2c93 100644 --- a/actions/setup/js/mcp_cli_bridge.test.cjs +++ b/actions/setup/js/mcp_cli_bridge.test.cjs @@ -222,7 +222,7 @@ describe("mcp_cli_bridge.cjs", () => { const outputLines = stdoutChunks.join("").trimEnd().split("\n"); expect(outputLines.length).toBeLessThanOrEqual(20); - expect(outputLines.join("\n")).toContain("... +"); + expect(outputLines.join("\n")).toMatch(/\.\.\. \+\d+ more command\(s\)/); }); it("keeps command help compact for many options", () => { @@ -244,7 +244,7 @@ describe("mcp_cli_bridge.cjs", () => { const outputLines = stdoutChunks.join("").trimEnd().split("\n"); expect(outputLines.length).toBeLessThanOrEqual(20); - expect(outputLines.join("\n")).toContain("... +"); + expect(outputLines.join("\n")).toMatch(/\.\.\. \+\d+ more option\(s\)/); expect(outputLines.join("\n")).toContain("Required options are marked with *."); }); From a1dd5048d561f50c99c6080da99bb6f0e767f420 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:24:19 +0000 Subject: [PATCH 04/10] Extract help text length constants Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index ccda70274ea..693635a2b39 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -56,6 +56,9 @@ const KEEPALIVE_PING_ID_START = 1000; /** Preferred max lines for generated CLI help output */ const HELP_MAX_LINES = 20; +const COMMAND_DESC_MAX_LEN = 68; +const TOOL_DESC_MAX_LEN = 90; +const OPTION_DESC_MAX_LEN = 62; // --------------------------------------------------------------------------- // Audit logging @@ -777,7 +780,7 @@ function showHelp(serverName, tools) { const maxCommandLines = Math.max(1, HELP_MAX_LINES - lines.length - reservedLines); const shownTools = tools.slice(0, maxCommandLines); for (const tool of shownTools) { - lines.push(` ${tool.name} — ${summarizeHelpText(tool.description || "No description", 68)}`); + lines.push(` ${tool.name} — ${summarizeHelpText(tool.description || "No description", COMMAND_DESC_MAX_LEN)}`); } if (tools.length > shownTools.length) { lines.push(` ... +${tools.length - shownTools.length} more command(s)`); @@ -808,7 +811,7 @@ function showToolHelp(serverName, toolName, tools) { const lines = [ `Command: ${toolName}`, - `Description: ${summarizeHelpText(tool.description || "No description", 90)}`, + `Description: ${summarizeHelpText(tool.description || "No description", TOOL_DESC_MAX_LEN)}`, `Usage: ${serverName} ${toolName} [--param value ...]`, `JSON mode: printf '{"param":"value",...}' | ${serverName} ${toolName} .`, ]; @@ -824,7 +827,7 @@ function showToolHelp(serverName, toolName, tools) { const shownOptions = optionEntries.slice(0, maxOptionLines); for (const [key, val] of shownOptions) { const requiredMark = required.has(key) ? "*" : ""; - const description = val.description ? ` - ${summarizeHelpText(val.description, 62)}` : ""; + const description = val.description ? ` - ${summarizeHelpText(val.description, OPTION_DESC_MAX_LEN)}` : ""; lines.push(` --${key} ${getTypeStr(val.type)}${requiredMark}${description}`); } if (optionEntries.length > shownOptions.length) { @@ -856,17 +859,14 @@ function getTypeStr(type) { * @returns {string} */ function summarizeHelpText(value, maxLen) { - if (!Number.isFinite(maxLen) || maxLen <= 0) { - return ""; - } const normalized = String(value || "") .replace(/\s+/g, " ") .trim(); - if (normalized.length <= maxLen) { + if (!Number.isFinite(maxLen) || maxLen <= 0) { return normalized; } - if (maxLen === 1) { - return "…"; + if (normalized.length <= maxLen) { + return normalized; } return `${normalized.slice(0, maxLen - 1)}…`; } From c538c02044e685cfc03ec629df3992dfe8f49253 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:24:55 +0000 Subject: [PATCH 05/10] Extract help reserved-line constants Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index 693635a2b39..50d779613e7 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -59,6 +59,8 @@ const HELP_MAX_LINES = 20; const COMMAND_DESC_MAX_LEN = 68; const TOOL_DESC_MAX_LEN = 90; const OPTION_DESC_MAX_LEN = 62; +const TOP_HELP_RESERVED_LINES = 3; +const TOOL_HELP_RESERVED_LINES = 2; // --------------------------------------------------------------------------- // Audit logging @@ -776,8 +778,7 @@ function showHelp(serverName, tools) { `Commands (${tools.length}):`, ]; if (tools.length > 0) { - const reservedLines = 3; // blank + footer + potential overflow marker - const maxCommandLines = Math.max(1, HELP_MAX_LINES - lines.length - reservedLines); + const maxCommandLines = Math.max(1, HELP_MAX_LINES - lines.length - TOP_HELP_RESERVED_LINES); const shownTools = tools.slice(0, maxCommandLines); for (const tool of shownTools) { lines.push(` ${tool.name} — ${summarizeHelpText(tool.description || "No description", COMMAND_DESC_MAX_LEN)}`); @@ -822,8 +823,7 @@ function showToolHelp(serverName, toolName, tools) { lines.push(""); lines.push(`Options (${Object.keys(props).length}):`); const optionEntries = Object.entries(props); - const reservedLines = 2; // footer + potential overflow marker - const maxOptionLines = Math.max(1, HELP_MAX_LINES - lines.length - reservedLines); + const maxOptionLines = Math.max(1, HELP_MAX_LINES - lines.length - TOOL_HELP_RESERVED_LINES); const shownOptions = optionEntries.slice(0, maxOptionLines); for (const [key, val] of shownOptions) { const requiredMark = required.has(key) ? "*" : ""; From d099bf796f859802a1cd25cf77dd887633a4d6e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:25:31 +0000 Subject: [PATCH 06/10] Show required-option marker only when needed Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index 50d779613e7..2a57b92e459 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -833,7 +833,9 @@ function showToolHelp(serverName, toolName, tools) { if (optionEntries.length > shownOptions.length) { lines.push(` ... +${optionEntries.length - shownOptions.length} more option(s)`); } - lines.push("Required options are marked with *."); + if (required.size > 0) { + lines.push("Required options are marked with *."); + } } process.stdout.write(lines.join("\n") + "\n"); From f2734ea546e21ac86c5d0a76562bb59f427b8234 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:53:21 +0000 Subject: [PATCH 07/10] Fix help-line budget edge cases and add exact-fit tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 32 +++++++------ actions/setup/js/mcp_cli_bridge.test.cjs | 60 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index 2a57b92e459..436dc942cc5 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -59,8 +59,7 @@ const HELP_MAX_LINES = 20; const COMMAND_DESC_MAX_LEN = 68; const TOOL_DESC_MAX_LEN = 90; const OPTION_DESC_MAX_LEN = 62; -const TOP_HELP_RESERVED_LINES = 3; -const TOOL_HELP_RESERVED_LINES = 2; +const TOP_HELP_FOOTER_LINES = 2; // --------------------------------------------------------------------------- // Audit logging @@ -771,15 +770,13 @@ function loadTools(toolsFile) { * @param {Array<{name: string, description?: string}>} tools - Tool list */ function showHelp(serverName, tools) { - const lines = [ - `Usage: ${serverName} [--param value ...]`, - `Tip: ${serverName} --help`, - "", - `Commands (${tools.length}):`, - ]; + const lines = [`Usage: ${serverName} [--param value ...]`, `Tip: ${serverName} --help`, "", `Commands (${tools.length}):`]; if (tools.length > 0) { - const maxCommandLines = Math.max(1, HELP_MAX_LINES - lines.length - TOP_HELP_RESERVED_LINES); - const shownTools = tools.slice(0, maxCommandLines); + const maxCommandLines = Math.max(1, HELP_MAX_LINES - lines.length - TOP_HELP_FOOTER_LINES); + let shownTools = tools.slice(0, maxCommandLines); + if (tools.length > shownTools.length && lines.length + shownTools.length + TOP_HELP_FOOTER_LINES + 1 > HELP_MAX_LINES) { + shownTools = shownTools.slice(0, -1); + } for (const tool of shownTools) { lines.push(` ${tool.name} — ${summarizeHelpText(tool.description || "No description", COMMAND_DESC_MAX_LEN)}`); } @@ -823,17 +820,24 @@ function showToolHelp(serverName, toolName, tools) { lines.push(""); lines.push(`Options (${Object.keys(props).length}):`); const optionEntries = Object.entries(props); - const maxOptionLines = Math.max(1, HELP_MAX_LINES - lines.length - TOOL_HELP_RESERVED_LINES); - const shownOptions = optionEntries.slice(0, maxOptionLines); + const maxOptionLines = Math.max(1, HELP_MAX_LINES - lines.length); + let shownOptions = optionEntries.slice(0, maxOptionLines); + let hasMoreOptions = optionEntries.length > shownOptions.length; + let hasVisibleRequired = shownOptions.some(([key]) => required.has(key)); + while (shownOptions.length > 0 && lines.length + shownOptions.length + (hasMoreOptions ? 1 : 0) + (hasVisibleRequired ? 1 : 0) > HELP_MAX_LINES) { + shownOptions = shownOptions.slice(0, -1); + hasMoreOptions = optionEntries.length > shownOptions.length; + hasVisibleRequired = shownOptions.some(([key]) => required.has(key)); + } for (const [key, val] of shownOptions) { const requiredMark = required.has(key) ? "*" : ""; const description = val.description ? ` - ${summarizeHelpText(val.description, OPTION_DESC_MAX_LEN)}` : ""; lines.push(` --${key} ${getTypeStr(val.type)}${requiredMark}${description}`); } - if (optionEntries.length > shownOptions.length) { + if (hasMoreOptions) { lines.push(` ... +${optionEntries.length - shownOptions.length} more option(s)`); } - if (required.size > 0) { + if (hasVisibleRequired) { lines.push("Required options are marked with *."); } } diff --git a/actions/setup/js/mcp_cli_bridge.test.cjs b/actions/setup/js/mcp_cli_bridge.test.cjs index 7319d3a2c93..22e2c4077ab 100644 --- a/actions/setup/js/mcp_cli_bridge.test.cjs +++ b/actions/setup/js/mcp_cli_bridge.test.cjs @@ -225,6 +225,19 @@ describe("mcp_cli_bridge.cjs", () => { expect(outputLines.join("\n")).toMatch(/\.\.\. \+\d+ more command\(s\)/); }); + it("does not truncate top-level help when commands exactly fit the line budget", () => { + const tools = Array.from({ length: 14 }, (_, i) => ({ + name: `tool_${i + 1}`, + description: `Description for command ${i + 1}.`, + })); + + showHelp("safeoutputs", tools); + + const outputLines = stdoutChunks.join("").trimEnd().split("\n"); + expect(outputLines.length).toBe(20); + expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more command\(s\)/); + }); + it("keeps command help compact for many options", () => { const properties = {}; for (let i = 1; i <= 24; i++) { @@ -246,6 +259,53 @@ describe("mcp_cli_bridge.cjs", () => { expect(outputLines.length).toBeLessThanOrEqual(20); expect(outputLines.join("\n")).toMatch(/\.\.\. \+\d+ more option\(s\)/); expect(outputLines.join("\n")).toContain("Required options are marked with *."); + expect(outputLines.join("\n")).toMatch(/--field_1 \(string\)\*/); + expect(outputLines.join("\n")).toMatch(/--field_2 \(string\)\*/); + }); + + it("does not truncate command help when options exactly fit the line budget", () => { + const properties = {}; + for (let i = 1; i <= 13; i++) { + properties[`field_${i}`] = { type: "string", description: `Field ${i}.` }; + } + + showToolHelp("safeoutputs", "create_issue", [ + { + name: "create_issue", + description: "Create an issue.", + inputSchema: { + properties, + required: ["field_1"], + }, + }, + ]); + + const outputLines = stdoutChunks.join("").trimEnd().split("\n"); + expect(outputLines.length).toBe(20); + expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more option\(s\)/); + expect(outputLines.join("\n")).toContain("Required options are marked with *."); + }); + + it("omits required-note when required options are truncated", () => { + const properties = {}; + for (let i = 1; i <= 24; i++) { + properties[`field_${i}`] = { type: "string", description: `Field ${i}.` }; + } + + showToolHelp("safeoutputs", "create_issue", [ + { + name: "create_issue", + description: "Create an issue.", + inputSchema: { + properties, + required: ["field_23", "field_24"], + }, + }, + ]); + + const outputLines = stdoutChunks.join("").trimEnd().split("\n"); + expect(outputLines.join("\n")).toMatch(/\.\.\. \+\d+ more option\(s\)/); + expect(outputLines.join("\n")).not.toContain("Required options are marked with *."); }); describe("stdin placeholder removed — '-' is always a literal value", () => { From fa8aaea56845401bf5f840c456a489da70ed2536 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:40:52 +0000 Subject: [PATCH 08/10] Compact help to show full command and option names Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 106 +++++++++++++---------- actions/setup/js/mcp_cli_bridge.test.cjs | 34 +++++--- 2 files changed, 83 insertions(+), 57 deletions(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index 436dc942cc5..d4761f56445 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -55,11 +55,10 @@ const KEEPALIVE_PING_INTERVAL_MS = 10000; const KEEPALIVE_PING_ID_START = 1000; /** Preferred max lines for generated CLI help output */ -const HELP_MAX_LINES = 20; -const COMMAND_DESC_MAX_LEN = 68; +const TOP_HELP_MAX_LINES = 20; +const TOOL_HELP_MAX_LINES = 30; const TOOL_DESC_MAX_LEN = 90; -const OPTION_DESC_MAX_LEN = 62; -const TOP_HELP_FOOTER_LINES = 2; +const COMPACT_NAME_LINE_TARGET_WIDTH = 110; // --------------------------------------------------------------------------- // Audit logging @@ -772,22 +771,16 @@ function loadTools(toolsFile) { function showHelp(serverName, tools) { const lines = [`Usage: ${serverName} [--param value ...]`, `Tip: ${serverName} --help`, "", `Commands (${tools.length}):`]; if (tools.length > 0) { - const maxCommandLines = Math.max(1, HELP_MAX_LINES - lines.length - TOP_HELP_FOOTER_LINES); - let shownTools = tools.slice(0, maxCommandLines); - if (tools.length > shownTools.length && lines.length + shownTools.length + TOP_HELP_FOOTER_LINES + 1 > HELP_MAX_LINES) { - shownTools = shownTools.slice(0, -1); - } - for (const tool of shownTools) { - lines.push(` ${tool.name} — ${summarizeHelpText(tool.description || "No description", COMMAND_DESC_MAX_LEN)}`); - } - if (tools.length > shownTools.length) { - lines.push(` ... +${tools.length - shownTools.length} more command(s)`); - } + const maxCommandLines = Math.max(1, TOP_HELP_MAX_LINES - lines.length); + lines.push( + ...formatCompactNameLines( + tools.map(tool => tool.name), + maxCommandLines + ) + ); } else { lines.push(" (tool list unavailable)"); } - lines.push(""); - lines.push(`Run '${serverName} --help' for option details.`); process.stdout.write(lines.join("\n") + "\n"); } @@ -820,24 +813,15 @@ function showToolHelp(serverName, toolName, tools) { lines.push(""); lines.push(`Options (${Object.keys(props).length}):`); const optionEntries = Object.entries(props); - const maxOptionLines = Math.max(1, HELP_MAX_LINES - lines.length); - let shownOptions = optionEntries.slice(0, maxOptionLines); - let hasMoreOptions = optionEntries.length > shownOptions.length; - let hasVisibleRequired = shownOptions.some(([key]) => required.has(key)); - while (shownOptions.length > 0 && lines.length + shownOptions.length + (hasMoreOptions ? 1 : 0) + (hasVisibleRequired ? 1 : 0) > HELP_MAX_LINES) { - shownOptions = shownOptions.slice(0, -1); - hasMoreOptions = optionEntries.length > shownOptions.length; - hasVisibleRequired = shownOptions.some(([key]) => required.has(key)); - } - for (const [key, val] of shownOptions) { - const requiredMark = required.has(key) ? "*" : ""; - const description = val.description ? ` - ${summarizeHelpText(val.description, OPTION_DESC_MAX_LEN)}` : ""; - lines.push(` --${key} ${getTypeStr(val.type)}${requiredMark}${description}`); - } - if (hasMoreOptions) { - lines.push(` ... +${optionEntries.length - shownOptions.length} more option(s)`); - } - if (hasVisibleRequired) { + const hasRequiredOptions = required.size > 0; + const maxOptionLines = Math.max(1, TOOL_HELP_MAX_LINES - lines.length - (hasRequiredOptions ? 1 : 0)); + lines.push( + ...formatCompactNameLines( + optionEntries.map(([key]) => `--${key}${required.has(key) ? "*" : ""}`), + maxOptionLines + ) + ); + if (hasRequiredOptions) { lines.push("Required options are marked with *."); } } @@ -845,18 +829,6 @@ function showToolHelp(serverName, toolName, tools) { process.stdout.write(lines.join("\n") + "\n"); } -/** - * Format a JSON schema type value as a short bracketed string. - * - * @param {string|string[]|undefined} type - * @returns {string} - */ -function getTypeStr(type) { - if (!type) return "(string)"; - const types = Array.isArray(type) ? type.filter(t => t !== "null") : [type]; - return `(${types.length > 0 ? types.join("|") : "null"})`; -} - /** * Collapse whitespace and trim long help text for compact output. * @@ -877,6 +849,46 @@ function summarizeHelpText(value, maxLen) { return `${normalized.slice(0, maxLen - 1)}…`; } +/** + * Render names as comma-separated compact lines and keep all names visible. + * + * @param {string[]} names + * @param {number} maxLines - Preferred line budget; non-positive/invalid values return one compact line + * @returns {string[]} + */ +function formatCompactNameLines(names, maxLines) { + if (!Array.isArray(names) || names.length === 0) { + return []; + } + if (!Number.isFinite(maxLines) || maxLines <= 0) { + return [` ${names.join(", ")}`]; + } + const lines = []; + let current = " "; + for (const name of names) { + const token = current.trim() ? `, ${name}` : name; + const shouldStartNewLine = current.length + token.length > COMPACT_NAME_LINE_TARGET_WIDTH; + if (shouldStartNewLine) { + lines.push(current); + current = ` ${name}`; + continue; + } + current += token; + } + if (current.trim()) { + lines.push(current); + } + if (lines.length > maxLines) { + // Keep all names visible by collapsing overflow into the last allowed line. + const compactTail = lines + .slice(maxLines - 1) + .map(line => line.trim()) + .join(", "); + return [...lines.slice(0, maxLines - 1), ` ${compactTail}`]; + } + return lines; +} + // --------------------------------------------------------------------------- // Response formatting // --------------------------------------------------------------------------- diff --git a/actions/setup/js/mcp_cli_bridge.test.cjs b/actions/setup/js/mcp_cli_bridge.test.cjs index 22e2c4077ab..dd83f950364 100644 --- a/actions/setup/js/mcp_cli_bridge.test.cjs +++ b/actions/setup/js/mcp_cli_bridge.test.cjs @@ -222,7 +222,10 @@ describe("mcp_cli_bridge.cjs", () => { const outputLines = stdoutChunks.join("").trimEnd().split("\n"); expect(outputLines.length).toBeLessThanOrEqual(20); - expect(outputLines.join("\n")).toMatch(/\.\.\. \+\d+ more command\(s\)/); + expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more command\(s\)/); + for (const tool of tools) { + expect(outputLines.join("\n")).toContain(tool.name); + } }); it("does not truncate top-level help when commands exactly fit the line budget", () => { @@ -234,8 +237,11 @@ describe("mcp_cli_bridge.cjs", () => { showHelp("safeoutputs", tools); const outputLines = stdoutChunks.join("").trimEnd().split("\n"); - expect(outputLines.length).toBe(20); + expect(outputLines.length).toBeLessThanOrEqual(20); expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more command\(s\)/); + for (const tool of tools) { + expect(outputLines.join("\n")).toContain(tool.name); + } }); it("keeps command help compact for many options", () => { @@ -256,11 +262,14 @@ describe("mcp_cli_bridge.cjs", () => { ]); const outputLines = stdoutChunks.join("").trimEnd().split("\n"); - expect(outputLines.length).toBeLessThanOrEqual(20); - expect(outputLines.join("\n")).toMatch(/\.\.\. \+\d+ more option\(s\)/); + expect(outputLines.length).toBeLessThanOrEqual(30); + expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more option\(s\)/); expect(outputLines.join("\n")).toContain("Required options are marked with *."); - expect(outputLines.join("\n")).toMatch(/--field_1 \(string\)\*/); - expect(outputLines.join("\n")).toMatch(/--field_2 \(string\)\*/); + for (let i = 1; i <= 24; i++) { + expect(outputLines.join("\n")).toContain(`--field_${i}`); + } + expect(outputLines.join("\n")).toContain("--field_1*"); + expect(outputLines.join("\n")).toContain("--field_2*"); }); it("does not truncate command help when options exactly fit the line budget", () => { @@ -281,12 +290,15 @@ describe("mcp_cli_bridge.cjs", () => { ]); const outputLines = stdoutChunks.join("").trimEnd().split("\n"); - expect(outputLines.length).toBe(20); + expect(outputLines.length).toBeLessThanOrEqual(30); expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more option\(s\)/); expect(outputLines.join("\n")).toContain("Required options are marked with *."); + for (let i = 1; i <= 13; i++) { + expect(outputLines.join("\n")).toContain(`--field_${i}`); + } }); - it("omits required-note when required options are truncated", () => { + it("keeps required-note when required options are in the compact list", () => { const properties = {}; for (let i = 1; i <= 24; i++) { properties[`field_${i}`] = { type: "string", description: `Field ${i}.` }; @@ -304,8 +316,10 @@ describe("mcp_cli_bridge.cjs", () => { ]); const outputLines = stdoutChunks.join("").trimEnd().split("\n"); - expect(outputLines.join("\n")).toMatch(/\.\.\. \+\d+ more option\(s\)/); - expect(outputLines.join("\n")).not.toContain("Required options are marked with *."); + expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more option\(s\)/); + expect(outputLines.join("\n")).toContain("Required options are marked with *."); + expect(outputLines.join("\n")).toContain("--field_23*"); + expect(outputLines.join("\n")).toContain("--field_24*"); }); describe("stdin placeholder removed — '-' is always a literal value", () => { From 6936fd9821173bbd2710d6fa20e0a18f73eebec1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:42:02 +0000 Subject: [PATCH 09/10] Document compact help soft-width behavior Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index d4761f56445..a54530fc2a6 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -851,9 +851,10 @@ function summarizeHelpText(value, maxLen) { /** * Render names as comma-separated compact lines and keep all names visible. + * Width is a soft target; the final line may exceed it to avoid dropping names. * * @param {string[]} names - * @param {number} maxLines - Preferred line budget; non-positive/invalid values return one compact line + * @param {number} maxLines - Preferred line budget; non-positive/invalid values force one compact line * @returns {string[]} */ function formatCompactNameLines(names, maxLines) { From f4a7fefb1fad4f79358533a29d6dad99a451eab8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:44:31 +0000 Subject: [PATCH 10/10] Polish compact help formatting and test assertions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 5 ++- actions/setup/js/mcp_cli_bridge.test.cjs | 39 +++++++++++++----------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index a54530fc2a6..b68015a49eb 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -859,6 +859,7 @@ function summarizeHelpText(value, maxLen) { */ function formatCompactNameLines(names, maxLines) { if (!Array.isArray(names) || names.length === 0) { + // Callers spread the result into help lines, so empty input should contribute no lines. return []; } if (!Number.isFinite(maxLines) || maxLines <= 0) { @@ -868,6 +869,7 @@ function formatCompactNameLines(names, maxLines) { let current = " "; for (const name of names) { const token = current.trim() ? `, ${name}` : name; + // A single very long name may still exceed the width target; we keep it intact. const shouldStartNewLine = current.length + token.length > COMPACT_NAME_LINE_TARGET_WIDTH; if (shouldStartNewLine) { lines.push(current); @@ -880,9 +882,10 @@ function formatCompactNameLines(names, maxLines) { lines.push(current); } if (lines.length > maxLines) { - // Keep all names visible by collapsing overflow into the last allowed line. + // Keep maxLines - 1 full lines and collapse the remaining names into the final allowed line. const compactTail = lines .slice(maxLines - 1) + // Trim per-line indentation before rebuilding a single indented tail line. .map(line => line.trim()) .join(", "); return [...lines.slice(0, maxLines - 1), ` ${compactTail}`]; diff --git a/actions/setup/js/mcp_cli_bridge.test.cjs b/actions/setup/js/mcp_cli_bridge.test.cjs index dd83f950364..587bb018930 100644 --- a/actions/setup/js/mcp_cli_bridge.test.cjs +++ b/actions/setup/js/mcp_cli_bridge.test.cjs @@ -221,10 +221,11 @@ describe("mcp_cli_bridge.cjs", () => { showHelp("safeoutputs", tools); const outputLines = stdoutChunks.join("").trimEnd().split("\n"); + const output = outputLines.join("\n"); expect(outputLines.length).toBeLessThanOrEqual(20); - expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more command\(s\)/); + expect(output).not.toMatch(/\.\.\. \+\d+ more command\(s\)/); for (const tool of tools) { - expect(outputLines.join("\n")).toContain(tool.name); + expect(output).toContain(tool.name); } }); @@ -237,10 +238,11 @@ describe("mcp_cli_bridge.cjs", () => { showHelp("safeoutputs", tools); const outputLines = stdoutChunks.join("").trimEnd().split("\n"); + const output = outputLines.join("\n"); expect(outputLines.length).toBeLessThanOrEqual(20); - expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more command\(s\)/); + expect(output).not.toMatch(/\.\.\. \+\d+ more command\(s\)/); for (const tool of tools) { - expect(outputLines.join("\n")).toContain(tool.name); + expect(output).toContain(tool.name); } }); @@ -262,14 +264,15 @@ describe("mcp_cli_bridge.cjs", () => { ]); const outputLines = stdoutChunks.join("").trimEnd().split("\n"); + const output = outputLines.join("\n"); expect(outputLines.length).toBeLessThanOrEqual(30); - expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more option\(s\)/); - expect(outputLines.join("\n")).toContain("Required options are marked with *."); + expect(output).not.toMatch(/\.\.\. \+\d+ more option\(s\)/); + expect(output).toContain("Required options are marked with *."); for (let i = 1; i <= 24; i++) { - expect(outputLines.join("\n")).toContain(`--field_${i}`); + expect(output).toContain(`--field_${i}`); } - expect(outputLines.join("\n")).toContain("--field_1*"); - expect(outputLines.join("\n")).toContain("--field_2*"); + expect(output).toContain("--field_1*"); + expect(output).toContain("--field_2*"); }); it("does not truncate command help when options exactly fit the line budget", () => { @@ -290,15 +293,16 @@ describe("mcp_cli_bridge.cjs", () => { ]); const outputLines = stdoutChunks.join("").trimEnd().split("\n"); + const output = outputLines.join("\n"); expect(outputLines.length).toBeLessThanOrEqual(30); - expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more option\(s\)/); - expect(outputLines.join("\n")).toContain("Required options are marked with *."); + expect(output).not.toMatch(/\.\.\. \+\d+ more option\(s\)/); + expect(output).toContain("Required options are marked with *."); for (let i = 1; i <= 13; i++) { - expect(outputLines.join("\n")).toContain(`--field_${i}`); + expect(output).toContain(`--field_${i}`); } }); - it("keeps required-note when required options are in the compact list", () => { + it("keeps required note when required options are in the compact list", () => { const properties = {}; for (let i = 1; i <= 24; i++) { properties[`field_${i}`] = { type: "string", description: `Field ${i}.` }; @@ -316,10 +320,11 @@ describe("mcp_cli_bridge.cjs", () => { ]); const outputLines = stdoutChunks.join("").trimEnd().split("\n"); - expect(outputLines.join("\n")).not.toMatch(/\.\.\. \+\d+ more option\(s\)/); - expect(outputLines.join("\n")).toContain("Required options are marked with *."); - expect(outputLines.join("\n")).toContain("--field_23*"); - expect(outputLines.join("\n")).toContain("--field_24*"); + const output = outputLines.join("\n"); + expect(output).not.toMatch(/\.\.\. \+\d+ more option\(s\)/); + expect(output).toContain("Required options are marked with *."); + expect(output).toContain("--field_23*"); + expect(output).toContain("--field_24*"); }); describe("stdin placeholder removed — '-' is always a literal value", () => {