From df2c08b546d4f40103e11b8b31dbfc3d41ee687b Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Mon, 12 Oct 2026 06:49:15 -0700 Subject: [PATCH] fix(jeb): confine an invalid REA_JEB_MCP_URL to the JEB tools (#2145) JebProvider throws when REA_JEB_MCP_URL fails endpoint validation, and the composition built it eagerly for the CLI and every MCP connection. A malformed value, or http://localhost:8425/mcp, therefore crashed every CLI command with a stack trace and failed every MCP connection with an internal server error. The composition now parses the endpoint first and, when it is unusable, returns a JEB port that reports not_configured availability and a ConfigurationError naming REA_JEB_MCP_URL. Fixes #2143 Co-authored-by: Claude Opus 5.5 --- src/composition/jeb.ts | 41 +++++++++++++++++++++++++++--- tests/boundary/cli/jebCli.test.ts | 11 ++++++++ tests/boundary/mcp/jebMcp.test.ts | 42 +++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/composition/jeb.ts b/src/composition/jeb.ts index 4abc33b9d..38f5dae5d 100644 --- a/src/composition/jeb.ts +++ b/src/composition/jeb.ts @@ -1,13 +1,48 @@ import type { JebAnalysisPort } from "../application/jeb/JebAnalysisPort.js"; +import { ConfigurationError } from "../domain/configurationErrors.js"; +import { err } from "../domain/result.js"; import type { JebMcpConnectionFactory } from "../jeb/JebMcpConnection.js"; +import { parseJebMcpEndpoint } from "../jeb/JebMcpEndpoint.js"; import { JebProvider } from "../jeb/JebProvider.js"; -/** Construct one JEB provider bound to the caller-selected MCP endpoint. */ +/** + * Construct one JEB provider bound to the caller-selected MCP endpoint. An + * unusable endpoint makes only JEB unavailable; every other CLI command and + * MCP tool is composed alongside it. + */ export const createJebAnalysisProvider = ( environment: Readonly> = process.env, connectionFactory?: JebMcpConnectionFactory, -): JebAnalysisPort => - new JebProvider({ +): JebAnalysisPort => { + const endpoint = parseJebMcpEndpoint(environment.REA_JEB_MCP_URL); + if (!endpoint.ok) { + const constraint = + typeof endpoint.error.cause === "string" + ? endpoint.error.cause + : endpoint.error.message; + return unconfiguredJebProvider( + new ConfigurationError("Invalid REA environment configuration", { + cause: endpoint.error, + settings: [{ setting: "REA_JEB_MCP_URL", constraint }], + }), + ); + } + return new JebProvider({ environment, ...(connectionFactory === undefined ? {} : { connectionFactory }), }); +}; + +const unconfiguredJebProvider = ( + error: ConfigurationError, +): JebAnalysisPort => ({ + inspectAvailability: () => + Promise.resolve({ + status: "unavailable", + code: "not_configured", + reason: `REA_JEB_MCP_URL is invalid: ${error.settings[0]?.constraint ?? error.message}`, + diagnostics: { setting: "REA_JEB_MCP_URL" }, + }), + close: () => Promise.resolve(), + execute: () => Promise.resolve(err(error)), +}); diff --git a/tests/boundary/cli/jebCli.test.ts b/tests/boundary/cli/jebCli.test.ts index c188b0e31..6e2325c7f 100644 --- a/tests/boundary/cli/jebCli.test.ts +++ b/tests/boundary/cli/jebCli.test.ts @@ -30,3 +30,14 @@ it("validates decompile item input before contacting the engine", async () => { ], }); }); + +it("reports an invalid endpoint without breaking other commands", async () => { + const environment = { REA_JEB_MCP_URL: "http://localhost:8425/mcp" }; + const jeb = await serveCli(["inspect-jeb-client", "--json"], environment); + expect(JSON.parse(jeb.stdout)).toMatchObject({ + code: "configuration_invalid", + details: { settings: [{ setting: "REA_JEB_MCP_URL" }] }, + }); + const capabilities = await serveCli(["capabilities", "--json"], environment); + expect(capabilities.exitCode).toBe(0); +}); diff --git a/tests/boundary/mcp/jebMcp.test.ts b/tests/boundary/mcp/jebMcp.test.ts index 7ccc0b579..44b482714 100644 --- a/tests/boundary/mcp/jebMcp.test.ts +++ b/tests/boundary/mcp/jebMcp.test.ts @@ -141,3 +141,45 @@ it("preserves JEB provider failure reasons through the MCP boundary", async () = expect(text).toContain("ECONNREFUSED"); expect(text).toContain("REA_JEB_MCP_URL"); }); + +it("keeps the MCP server usable when REA_JEB_MCP_URL is invalid", async () => { + const session = createTestBinarySession(() => { + throw new Error("deep provider must not start"); + }); + const server = createServer( + { kind: "session", session }, + { environment: { REA_JEB_MCP_URL: "http://localhost:8425/mcp" } }, + ); + const client = new Client({ name: "jeb-invalid-endpoint", version: "1" }); + onTestFinished(async () => { + await client.close(); + await server.close(); + await session.close(); + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + expect((await client.listTools()).tools.length).toBeGreaterThan(0); + const failed = await client.callTool({ + name: "inspect_jeb_client", + arguments: {}, + }); + expect(failed.isError).toBe(true); + const text = (failed.content ?? []) + .map((block) => (block.type === "text" ? block.text : "")) + .join(""); + expect(JSON.parse(text)).toMatchObject({ + error: { + code: "configuration_invalid", + details: { + settings: [ + { + setting: "REA_JEB_MCP_URL", + constraint: expect.stringContaining("loopback"), + }, + ], + }, + }, + }); +});