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 <noreply@anthropic.com>
This commit is contained in:
parent
58fb372071
commit
df2c08b546
|
|
@ -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<Record<string, string | undefined>> = 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)),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue