fix(mcp): normalize browser scenario input errors (#4661)

Co-authored-by: gongfpp <22683417+gongfpp@users.noreply.github.com>
This commit is contained in:
gongfppandgongfpp authored and GitHub committed 2026-10-17 17:36:32 +08:00
1 parent 9534e42ec8
commit 5a7e8a166a
3 files changed
+149 -5

No files matched your search

+4 -4
View File
@@ -6,11 +6,10 @@ import { withCommandCancellation } from "./cli/commandCancellation.js";
import { CLI_COMMANDS } from "./cliCommandNames.js";
import { parseCliJsonInput, resolveCliJsonPaths } from "./cliJsonInput.js";
import { logCliCommand } from "./cliLogging.js";
import { AnalysisInputError } from "./domain/analysisErrorCore.js";
import { AnalysisError } from "./domain/analysisErrorBase.js";
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
import { BrowserObservationError } from "./domain/browserObservationError.js";
import { projectInputIssues } from "./domain/inputIssueProjection.js";
import { analysisInputErrorFromIssues } from "./domain/inputIssueProjection.js";
import { browserScenarioSchema } from "./domain/browserScenario.js";
import type { JsonValue } from "./domain/jsonValue.js";
import type { Logger } from "pino";
@@ -41,10 +40,11 @@ export const registerBrowserScenarioCommands = (
);
if (!scenario.success)
return cliError(
new AnalysisInputError(
analysisInputErrorFromIssues(
OPERATION,
scenario.error.issues,
input.value,
{ cause: scenario.error },
projectInputIssues(scenario.error.issues, input.value),
),
);
// Load Playwright only for this command, not during CLI startup.
+15 -1
View File
@@ -9,6 +9,7 @@ import { err } from "../domain/result.js";
import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
import { captureBrowserScenario } from "../application/BrowserScenarioCaptureService.js";
import { toolContract } from "../contracts/toolContracts.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { browserScenarioSchema } from "../domain/browserScenario.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
@@ -35,7 +36,20 @@ export const registerBrowserScenarioTool = (
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const scenario = browserScenarioSchema.parse(input);
const parsedScenario = browserScenarioSchema.safeParse(input);
if (!parsedScenario.success)
return server.delivery.toCallToolResult(
err(
analysisInputErrorFromIssues(
contract.name,
parsedScenario.error.issues,
input,
{ cause: parsedScenario.error },
),
),
contract,
);
const scenario = parsedScenario.data;
if (options.loadFailure !== undefined)
return server.delivery.toCallToolResult(
err(optionalProviderUnavailable(options.loadFailure, contract.name)),
@@ -0,0 +1,130 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { Ajv2020 } from "ajv/dist/2020.js";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const scenarios = [
{
name: "undeclared secret references",
issuePath: ["secrets"],
issueMessage: "Secret reference pw is not declared",
actions: [
{
step_id: "pw",
action: "fill",
locator: { kind: "css", selector: "#pw" },
value: { source: "secret", secret_id: "pw" },
},
],
},
{
name: "duplicate action step IDs",
issuePath: ["actions"],
issueMessage: "Action step IDs must be unique",
actions: [
{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 },
{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 },
],
},
] as const;
cliTest(
"projects refined browser scenario validation into CLI-equivalent diagnostics over stdio MCP",
async ({ cli }) => {
const home = await createTestTempDirectory(
"rea-browser-scenario-mcp-home-",
);
const client = new Client({
name: "browser-scenario-input-diagnostics",
version: "1",
});
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: {
PATH: process.env.PATH ?? "",
HOME: home,
REA_LOG_LEVEL: "silent",
},
stderr: "pipe",
});
transport.stderr?.on("data", () => undefined);
try {
await client.connect(transport);
const tool = (await client.listTools()).tools.find(
({ name }) => name === "capture_browser_scenario",
);
if (tool === undefined) throw new Error("Missing browser scenario tool");
const validate = new Ajv2020({
strict: false,
validateFormats: false,
}).compile(tool.inputSchema);
for (const scenario of scenarios) {
const input = {
browser: {
mode: "launch",
executable_path: join(home, "missing-chromium"),
},
start_url: { url: "http://127.0.0.1:1/", query: [] },
actions: scenario.actions,
secrets: [],
};
expect(
validate(input),
`${scenario.name}: ${JSON.stringify(validate.errors)}`,
).toBe(true);
const cliResult = await cli.run({
arguments: [
"capture-browser-scenario",
JSON.stringify(input),
"--json",
],
environment: { HOME: home, REA_LOG_LEVEL: "silent" },
});
expect(cliResult.exitCode).toBe(1);
const cliDiagnostic = z
.record(z.string(), z.unknown())
.parse(cliResult.json);
expect(typeof cliDiagnostic.error).toBe("string");
delete cliDiagnostic.error;
expect(cliDiagnostic).toMatchObject({
code: "invalid_request",
category: "invalid_input",
retryable: true,
remediation: { action: "Correct the listed arguments and retry." },
details: {
operation: "capture_browser_scenario",
issues: [
{
path: scenario.issuePath,
reason: "invalid_value",
message: scenario.issueMessage,
},
],
},
});
const mcpResult = await client.callTool({
name: tool.name,
arguments: input,
});
expect(mcpResult.structuredContent).toBeUndefined();
expect(parseMcpToolError(mcpResult).error).toEqual(cliDiagnostic);
await expect(client.ping()).resolves.toEqual({});
}
} finally {
await Promise.all([client.close(), transport.close()]);
}
},
60_000,
);