fix(cli): print compact mcp doctor JSON when stdout is redirected (#5514)

rea mcp doctor rendered json with Formatter.format, which always indents, so
redirected --json, --full-output and validation errors stayed indented. The
dispatcher now passes the stdout TTY state and the doctor renders json and
jsonl through jsonOutputParts like the other commands.

Fixes #5435

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
Wadeandgensokyo-dev authored and GitHub committed 2026-10-23 02:23:39 +08:00
1 parent 5c8d5b2a8a
commit d11fca46fc
4 files changed
+56 -5

No files matched your search

+1
View File
@@ -76,6 +76,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
const result = await runProductionMcpDoctorCli(args.slice(2), {
dispatcherPath: fileURLToPath(import.meta.url),
packageRoot,
stdoutIsTerminal: process.stdout.isTTY === true,
});
writeStdout(result.output);
process.exitCode = result.exitCode;
+19 -4
View File
@@ -6,6 +6,7 @@ import { resolve } from "node:path";
import { StringDecoder } from "node:string_decoder";
import { createCatalogIdentity } from "./catalogIdentity.js";
import { jsonOutputParts } from "./cli/jsonOutput.js";
import {
compactPresentationForMcpInputSchemaProfile,
parseMcpInputSchemaProfile,
@@ -324,7 +325,11 @@ const inspectProductionMcpSession = async (
*/
export const runProductionMcpDoctorCli = async (
arguments_: readonly string[],
input: { readonly dispatcherPath: string; readonly packageRoot: string },
input: {
readonly dispatcherPath: string;
readonly packageRoot: string;
readonly stdoutIsTerminal: boolean;
},
): Promise<{ readonly output: string; readonly exitCode: number }> => {
const started = performance.now();
const parsed = parseOutputArguments(arguments_);
@@ -332,10 +337,11 @@ export const runProductionMcpDoctorCli = async (
if (parsed.help)
return { output: `${renderMcpDoctorHelp()}\n`, exitCode: 0 };
return {
output: `${Formatter.format(
output: renderOutput(
{ code: "VALIDATION_ERROR", message: parsed.message },
parsed.format,
)}\n`,
input.stdoutIsTerminal,
),
exitCode: 1,
};
}
@@ -356,7 +362,7 @@ export const runProductionMcpDoctorCli = async (
}
: result;
return {
output: `${Formatter.format(outputValue, parsed.format)}\n`,
output: renderOutput(outputValue, parsed.format, input.stdoutIsTerminal),
exitCode: result.healthy ? 0 : 1,
};
};
@@ -439,6 +445,15 @@ type McpDoctorArguments =
readonly message: string;
};
const renderOutput = (
value: unknown,
format: OutputFormat,
stdoutIsTerminal: boolean,
): string =>
format === "json" || format === "jsonl"
? [...jsonOutputParts(value, format, stdoutIsTerminal)].join("")
: `${Formatter.format(value, format)}\n`;
const parseOutputArguments = (
arguments_: readonly string[],
): McpDoctorArguments => {
+3 -1
View File
@@ -79,7 +79,7 @@ describe("executable dispatcher", () => {
),
workspace.write(
"dist/cli-runtime/mcpDoctor.js",
'export const runProductionMcpDoctorCli = (args) => Promise.resolve({ output: JSON.stringify({ adapter: "mcp-doctor", args }), exitCode: 0 });\n',
'export const runProductionMcpDoctorCli = (args, input) => Promise.resolve({ output: JSON.stringify({ adapter: "mcp-doctor", args, stdoutIsTerminal: input.stdoutIsTerminal }), exitCode: 0 });\n',
),
]);
@@ -98,6 +98,7 @@ describe("executable dispatcher", () => {
await expect(invoke(["mcp", "doctor", "--json"])).resolves.toEqual({
adapter: "mcp-doctor",
args: ["--json"],
stdoutIsTerminal: false,
});
await expect(invoke(["mcp", "add"])).resolves.toEqual({
adapter: "cli",
@@ -120,6 +121,7 @@ describe("executable dispatcher", () => {
await expect(invoke(["mcp", "doctor", "--http"])).resolves.toEqual({
adapter: "mcp-doctor",
args: ["--http"],
stdoutIsTerminal: false,
});
await expect(invoke(["mcp", "add", "--http"])).resolves.toEqual({
adapter: "cli",
+33
View File
@@ -83,6 +83,7 @@ describe("production MCP doctor", () => {
{
dispatcherPath: resolve("tests/fixtures/mcpDoctorExit.mjs"),
packageRoot: process.cwd(),
stdoutIsTerminal: false,
},
);
expect(result.exitCode).toBe(1);
@@ -191,6 +192,38 @@ describe("production MCP doctor", () => {
);
});
describe("production MCP doctor output", () => {
it.each([
{ name: "report", args: ["--json"] },
{ name: "full-output envelope", args: ["--json", "--full-output"] },
{ name: "validation error", args: ["--json", "--unknown"] },
])("indents $name JSON only for a terminal stdout", async ({ args }) => {
const render = (stdoutIsTerminal: boolean) =>
runProductionMcpDoctorCli(args, {
dispatcherPath: resolve("tests/fixtures/mcpDoctorExit.mjs"),
packageRoot: process.cwd(),
stdoutIsTerminal,
});
const [redirected, terminal] = await Promise.all([
render(false),
render(true),
]);
expect(redirected.output.split("\n")).toHaveLength(2);
expect(redirected.output.endsWith("\n")).toBe(true);
expect(terminal.output).toContain('\n "');
const withoutDuration = (output: string): unknown => {
const value = JSON.parse(output) as { meta?: { duration?: string } };
if (value.meta) delete value.meta.duration;
return value;
};
expect(withoutDuration(redirected.output)).toEqual(
withoutDuration(terminal.output),
);
expect(redirected.exitCode).toBe(terminal.exitCode);
});
});
describe("production MCP doctor probe validation", () => {
it("rejects a schema-invalid probe while retaining the discovered inventory", async () => {
const root = await createTestTempDirectory("rea-mcp-doctor-reply-");