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:
1 parent
5c8d5b2a8a
commit
d11fca46fc
4 files changed
+56
-5
No files matched your search
@@ -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
@@ -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 => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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-");
|
||||
|
||||
Reference in new issue
Block a user