fix(providers): preserve reply bytes and structured JSON ownership

Share strict UTF-8 JSON admission across bounded worker reports. Preserve IDA/JEB structured dictionaries through negotiated SDK validation and adapter decoding, retaining inherited tool validation and protocol handling.

Validated 240 tests, including real SDK/HTTP exchanges and firmware file reads; 83 host-specific cases skipped. Typecheck, lint, and discovery passed. Real-provider execution and modern MCP protocol behavior remain unverified.
This commit is contained in:
morluto
2026-10-26 17:52:11 +08:00
parent 721dd3d1aa
commit da526fc014
18 changed files with 508 additions and 95 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ export const SOURCE_TEST_PROJECTS = [
group: "source",
include: [
"src/*.test.ts",
"src/{android,apkSignature,apktool,jeb,adb,artifacts,browser,config,cutter,dotnet,electron,erlang,evm,firmware,flutter,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,main,native,process,python,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts",
"src/{android,apkSignature,apktool,jeb,adb,artifacts,browser,config,cutter,dotnet,electron,erlang,evm,firmware,flutter,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,main,native,process,providers,python,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts",
],
},
{
+28
View File
@@ -0,0 +1,28 @@
import { expect, it } from "vitest";
import { safeParseUtf8Json } from "./safeJson.js";
it("distinguishes malformed UTF-8 from a literal Unicode replacement character", () => {
const invalid = Buffer.concat([
Buffer.from('{"message":"'),
Buffer.from([0xff]),
Buffer.from('"}'),
]);
expect(safeParseUtf8Json(invalid)).toMatchObject({
ok: false,
cause: { code: "ERR_ENCODING_INVALID_ENCODED_DATA" },
});
expect(safeParseUtf8Json(Buffer.from('{"message":"valid � 雪"}'))).toEqual({
ok: true,
value: { message: "valid � 雪" },
});
});
it.each(["\uFEFF{}", '{"message":', ""])(
"retains strict JSON failure causes for %j",
(text) => {
expect(safeParseUtf8Json(Buffer.from(text))).toMatchObject({
ok: false,
cause: expect.any(SyntaxError),
});
},
);
+17
View File
@@ -3,6 +3,23 @@ export type SafeJsonParseResult =
| { readonly ok: true; readonly value: unknown }
| { readonly ok: false; readonly error: string; readonly cause: unknown };
/** Decode complete UTF-8 JSON without replacing malformed bytes or stripping a BOM. */
export const safeParseUtf8Json = (bytes: Uint8Array): SafeJsonParseResult => {
let text: string;
try {
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
bytes,
);
} catch (cause: unknown) {
return {
ok: false,
error: cause instanceof Error ? cause.message : String(cause),
cause,
};
}
return safeParseJson(text);
};
/** Parse JSON text, returning the failure reason instead of throwing. */
export const safeParseJson = (
value: string,
+5 -2
View File
@@ -1,3 +1,4 @@
import { safeParseUtf8Json } from "../domain/safeJson.js";
import { createHash, randomUUID } from "node:crypto";
import { access, readFile, stat, writeFile } from "node:fs/promises";
import { constants } from "node:fs";
@@ -42,8 +43,8 @@ import { evmInterfaceEvidence } from "../domain/evm/evmInterfaceEvidence.js";
import {
DEFAULT_EVM_TIMEOUT_MS,
type InspectEvmInterfaceInput,
evmInterfaceSchema,
} from "../domain/evm/evmInterface.js";
import { evmInterfaceSchema } from "../domain/evm/evmInterface.js";
import { solidityMetadataSchema } from "../domain/evm/solidityMetadata.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
@@ -569,7 +570,9 @@ export class EvmoleInterfaceProvider implements EvmInterfacePort {
undefined,
this.#resources,
);
reply = replySchema.parse(JSON.parse(file.bytes.toString("utf8")));
const decoded = safeParseUtf8Json(file.bytes);
if (!decoded.ok) throw decoded.cause;
reply = replySchema.parse(decoded.value);
} catch (cause: unknown) {
const cleanup =
cause instanceof ArtifactReaderFailure ? cause.cleanup : undefined;
+4 -6
View File
@@ -1,3 +1,4 @@
import { safeParseUtf8Json } from "../domain/safeJson.js";
import { createHash } from "node:crypto";
import { createWriteStream } from "node:fs";
import { chmod, lstat, opendir, readlink, realpath } from "node:fs/promises";
@@ -264,12 +265,9 @@ export class FirmwareFileAccess {
if (bytes.length === 0)
throw firmwareReportFailure(operation, "empty", "Report is empty");
try {
const value: unknown = JSON.parse(
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
bytes,
),
);
return value;
const decoded = safeParseUtf8Json(bytes);
if (!decoded.ok) throw decoded.cause;
return decoded.value;
} catch (cause: unknown) {
throw firmwareReportFailure(
operation,
+50
View File
@@ -645,3 +645,53 @@ it("keeps a real SDK physical failure when abort arrives during its outer unwind
await connection.close();
}
});
it("preserves observed own structured members before deciding whether to unwrap a result", () => {
const structured: unknown = JSON.parse(
'{"__proto__":{"observed":true},"result":"field","nested":[{"__proto__":"nested fact"}]}',
);
const decoded = decodeIdaToolResult({
content: [],
structuredContent: structured,
});
expect(Object.hasOwn(decoded as object, "__proto__")).toBe(true);
expect(JSON.stringify(decoded)).toBe(JSON.stringify(structured));
const wrapped: unknown = JSON.parse(
'{"result":{"__proto__":{"observed":true},"nested":{"__proto__":"nested fact"}}}',
);
const value = decodeIdaToolResult({
content: [],
structuredContent: wrapped,
});
expect(Object.hasOwn(value as object, "__proto__")).toBe(true);
expect(JSON.stringify(value)).toBe(
'{"__proto__":{"observed":true},"nested":{"__proto__":"nested fact"}}',
);
});
it("keeps error, truncation and native-text precedence ahead of structured JSON admission", () => {
const structuredContent = { result: Infinity };
expect(() =>
decodeIdaToolResult({
isError: true,
structuredContent,
content: [{ type: "text", text: "engine refused" }],
}),
).toThrow(/IDA MCP tool failed: \s*engine refused/u);
expect(() =>
decodeIdaToolResult({
structuredContent,
content: [],
_meta: { ida_mcp: { output_truncated: true } },
}),
).toThrow("IDA MCP truncated its output");
expect(
decodeIdaToolResult(
{
structuredContent,
content: [{ type: "text", text: '{"complete":true}' }],
},
"native",
),
).toEqual({ complete: true });
});
+17 -10
View File
@@ -1,3 +1,4 @@
import { StructuredContentMcpClient } from "../providers/StructuredContentMcpClient.js";
import {
createIdaDiagnosticRedactor,
IDA_DIAGNOSTIC_SOURCE_BYTES,
@@ -8,7 +9,6 @@ import { ChildProcess } from "node:child_process";
import { Socket } from "node:net";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import {
Client,
ProtocolError,
SdkError,
SdkErrorCode,
@@ -24,7 +24,11 @@ import {
idaRegistrationTimeoutGuidance,
type IdaConfiguration,
} from "./IdaConfiguration.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import {
jsonObjectSchema,
jsonValueSchema,
type JsonValue,
} from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import {
AnalysisTimeoutError,
@@ -106,6 +110,8 @@ export const decodeIdaToolResult = (
diagnostic: IdaDiagnosticRedactor = createIdaDiagnosticRedactor(),
): JsonValue => {
const result = toolResultSchema.parse(input);
// Shape validation's record copy can omit own __proto__ members; JSON owns the original dictionary.
const source = input as z.input<typeof toolResultSchema>;
const blocks = result.content.flatMap((block) =>
block.type === "text" && block.text !== undefined ? [block.text] : [],
);
@@ -138,13 +144,11 @@ export const decodeIdaToolResult = (
);
return parseIdaJsonText(text);
}
if (result.structuredContent !== undefined) {
const value = result.structuredContent;
return jsonValueSchema.parse(
Object.keys(value).length === 1 && "result" in value
? value.result
: value,
);
if (source.structuredContent !== undefined) {
const value = jsonObjectSchema.parse(source.structuredContent);
return Object.keys(value).length === 1 && Object.hasOwn(value, "result")
? value.result!
: value;
}
if (text === undefined)
throw new AnalysisProtocolError(
@@ -360,7 +364,10 @@ export const createIdaMcpConnection = (
environment: Readonly<NodeJS.ProcessEnv>,
dependencies: IdaMcpConnectionDependencies = {},
): IdaMcpConnection => {
const client = new Client({ name: "rea-ida-provider", version: "1" });
const client = new StructuredContentMcpClient({
name: "rea-ida-provider",
version: "1",
});
const redactor = (
dependencies.createDiagnosticRedactor ?? createIdaDiagnosticRedactor
)(config);
+16
View File
@@ -446,3 +446,19 @@ it("passes a discovery budget above the SDK default to connect and every catalog
}
}
});
it.each([false, true])(
"retains structured own members and nested dictionaries in a JEB reply (isError=%s)",
(isError) => {
const structured: unknown = JSON.parse(
'{"__proto__":{"observed":true},"success":false,"message":"missing","nested":[{"__proto__":"nested fact"}]}',
);
const decoded = decodeJebToolResult({
isError,
structuredContent: structured,
content: [],
});
expect(Object.hasOwn(decoded as object, "__proto__")).toBe(true);
expect(JSON.stringify(decoded)).toBe(JSON.stringify(structured));
},
);
+9 -3
View File
@@ -3,7 +3,11 @@ import {
DEFAULT_REQUEST_TIMEOUT_MSEC,
} from "@modelcontextprotocol/client";
import { z } from "zod";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import {
jsonObjectSchema,
jsonValueSchema,
type JsonValue,
} from "../domain/jsonValue.js";
import {
AnalysisCancelledError,
AnalysisProtocolError,
@@ -57,12 +61,14 @@ const toolResultSchema = z.object({
/** Decode a JEB MCP tool result without inventing an empty observation. */
export const decodeJebToolResult = (input: unknown): JsonValue => {
const result = toolResultSchema.parse(input);
// Shape validation's record copy can omit own __proto__ members; JSON owns the original dictionary.
const source = input as z.input<typeof toolResultSchema>;
const blocks = result.content.flatMap((block) =>
block.type === "text" && block.text !== undefined ? [block.text] : [],
);
let decoded: JsonValue;
if (result.structuredContent !== undefined) {
decoded = jsonValueSchema.parse(result.structuredContent);
if (source.structuredContent !== undefined) {
decoded = jsonObjectSchema.parse(source.structuredContent);
} else {
if (blocks.length !== 1)
throw new AnalysisProtocolError(
+5 -2
View File
@@ -1,5 +1,5 @@
import { StructuredContentMcpClient } from "../providers/StructuredContentMcpClient.js";
import {
Client,
SdkError,
SdkErrorCode,
StreamableHTTPClientTransport,
@@ -53,7 +53,10 @@ export class JebMcpAcquisitionCleanupFailure extends ProviderCleanupError {
/** One SDK client owns its attached transport; failed cleanup retains both identities. */
export class JebMcpConnectionOwner {
readonly client = new Client({ name: "rea-jeb", version: "0.0.0" });
readonly client = new StructuredContentMcpClient({
name: "rea-jeb",
version: "0.0.0",
});
readonly transport: StreamableHTTPClientTransport;
readonly signal = new AbortController();
tools: readonly string[] | undefined;
+4 -1
View File
@@ -1,3 +1,4 @@
import { safeParseUtf8Json } from "../../domain/safeJson.js";
import {
AnalysisCleanupLease,
AnalysisCleanupLeaseGroup,
@@ -387,7 +388,9 @@ export class PwntoolsDecoder<
undefined,
this.#resources,
);
reply = replySchema.parse(JSON.parse(file.bytes.toString("utf8")));
const decoded = safeParseUtf8Json(file.bytes);
if (!decoded.ok) throw decoded.cause;
reply = replySchema.parse(decoded.value);
} catch (cause: unknown) {
if (
cause instanceof ArtifactReaderFailure &&
@@ -1,12 +1,13 @@
import { safeParseUtf8Json } from "../../domain/safeJson.js";
import { join } from "node:path";
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
import {
PWNTOOLS_MEMORY_FAILURE_EXIT,
PWNTOOLS_FILE_SIZE_FAILURE_EXIT,
PWNTOOLS_LIMITS,
} from "./PwntoolsRelease.js";
import { z } from "zod";
import { PWNTOOLS_LIMITS } from "./PwntoolsRelease.js";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
/** Actual limits reported by the owned bridge after lowering inherited soft limits. */
@@ -94,10 +95,10 @@ export const readPwntoolsFailureEvidence = async (
undefined,
resources,
);
const decoded = safeParseUtf8Json(reported.bytes);
if (!decoded.ok) throw decoded.cause;
limitReport = {
limits: pwntoolsResourceLimitsSchema.parse(
JSON.parse(reported.bytes.toString("utf8")),
),
limits: pwntoolsResourceLimitsSchema.parse(decoded.value),
failure: null,
};
} catch (reportFailure: unknown) {
@@ -0,0 +1,111 @@
import { InMemoryTransport, type Tool } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { z } from "zod";
import { StructuredContentMcpClient } from "./StructuredContentMcpClient.js";
/** Raw peer replies pass through the actual SDK protocol and era validator. */
const exchange = async (
reply: Record<string, unknown>,
run: (client: StructuredContentMcpClient) => Promise<void>,
) => {
const client = new StructuredContentMcpClient({
name: "structured-content-test",
version: "1",
});
const [local, peer] = InMemoryTransport.createLinkedPair();
peer.onmessage = async (message) => {
if (
!("method" in message) ||
!("id" in message) ||
message.id === undefined
)
return;
const result =
message.method === "initialize"
? {
protocolVersion: "2025-11-25",
capabilities: { tools: {} },
serverInfo: { name: "raw-peer", version: "1" },
}
: message.method === "tools/list"
? { tools: [] }
: message.method === "ping"
? {}
: reply;
await peer.send({ jsonrpc: "2.0", id: message.id, result });
};
try {
await peer.start();
await client.connect(local);
await run(client);
} finally {
await client.close();
await peer.close();
}
};
it("preserves structured own properties while retaining legacy default content", async () => {
const structuredContent: unknown = JSON.parse(
'{"__proto__":{"observed":true},"result":"field"}',
);
await exchange({ structuredContent }, async (client) => {
const result = await client.callTool({ name: "observed", arguments: {} });
expect(result.content).toEqual([]);
expect(Object.hasOwn(result.structuredContent as object, "__proto__")).toBe(
true,
);
expect(JSON.stringify(result.structuredContent)).toBe(
JSON.stringify(structuredContent),
);
});
});
it.each(["task", "inputRequests", "requestState"])(
"retains the SDK's legacy contentless foreign-family rejection for %s",
async (key) => {
await exchange({ [key]: {} }, async (client) => {
await expect(
client.callTool({ name: "foreign", arguments: {} }),
).rejects.toThrow(/content is required/u);
});
},
);
it("retains inherited callTool output-schema validation", async () => {
const tool: Tool = {
name: "typed",
inputSchema: { type: "object" },
outputSchema: {
type: "object",
properties: { count: { type: "integer" } },
required: ["count"],
},
};
await exchange(
{ content: [], structuredContent: { count: "wrong" } },
async (client) => {
await expect(
client.callTool(
{ name: tool.name, arguments: {} },
{ toolDefinition: tool },
),
).rejects.toThrow(/does not match the tool's output schema/u);
},
);
});
it("delegates explicitly supplied result schemas and other method requests unchanged", async () => {
const reply = { observed: "custom result" };
const schema = z
.strictObject({ observed: z.string() })
.transform(({ observed }) => observed);
await exchange(reply, async (client) => {
expect(
await client.request(
{ method: "tools/call", params: { name: "custom", arguments: {} } },
schema,
),
).toBe("custom result");
expect(await client.ping()).toEqual({});
});
});
@@ -0,0 +1,77 @@
import {
Client,
type CallToolResult,
type Request,
type RequestMethod,
type RequestOptions,
type ResultTypeMap,
type StandardSchemaV1,
} from "@modelcontextprotocol/client";
const isSchema = (
value: RequestOptions | StandardSchemaV1 | undefined,
): value is StandardSchemaV1 =>
typeof value === "object" && value !== null && "~standard" in value;
/** Preserve observed structured dictionaries across the SDK's era-specific result validation. */
export class StructuredContentMcpClient extends Client {
readonly #toolResult: StandardSchemaV1<unknown, CallToolResult> = {
"~standard": {
version: 1,
vendor: "rea-mcp-structured-content",
validate: (input) => {
const parsed = this._wireCodec().validateResult("tools/call", input);
if (!parsed.ok)
return {
issues: [
{
message:
parsed.reason === "invalid"
? parsed.message
: "not-in-era: tools/call",
},
],
};
// Retain the SDK's envelope normalization, replacing only the dictionary
// its legacy record parser may clone without own __proto__ members.
if (
typeof input === "object" &&
input !== null &&
Object.hasOwn(input, "structuredContent")
)
return {
value: {
...parsed.value,
structuredContent: (input as { structuredContent: unknown })
.structuredContent,
},
};
return { value: parsed.value };
},
},
};
override request<Method extends RequestMethod>(
request: { method: Method; params?: Record<string, unknown> },
options?: RequestOptions,
): Promise<ResultTypeMap[Method]>;
override request<Schema extends StandardSchemaV1>(
request: Request,
resultSchema: Schema,
options?: RequestOptions,
): Promise<StandardSchemaV1.InferOutput<Schema>>;
override request(
request: Request,
schemaOrOptions?: RequestOptions | StandardSchemaV1,
options?: RequestOptions,
): Promise<unknown> {
if (isSchema(schemaOrOptions))
return super.request(request, schemaOrOptions, options);
if (request.method === "tools/call")
return super.request(request, this.#toolResult, schemaOrOptions);
return super.request(
request as { method: RequestMethod; params?: Record<string, unknown> },
schemaOrOptions,
);
}
}
@@ -519,6 +519,9 @@ it
"output-limit",
"wrong-profile",
"malformed-reply",
"invalid-utf8",
"literal-replacement",
"bom-reply",
])(
"preserves process/reply boundaries and cleans private snapshots: %s",
async (scenario) => {
@@ -542,32 +545,50 @@ it
diagnostics: _diagnostics,
...value
} = binaryLayoutFixture(path);
await writeFile(
request.reply_path,
scenario === "malformed-reply"
? "broken-json"
: JSON.stringify(
[
"format",
"unavailable",
"unsupported",
"output-limit",
].includes(scenario)
? {
ok: false,
reason: scenario,
message: "ELF table is truncated.",
}
: {
ok: true,
profile:
scenario === "wrong-profile"
? "pwntools@other"
: PWNTOOLS_PROVIDER_IDENTITY.version,
value,
},
),
);
const encodedReplies: Partial<Record<string, string | Buffer>> = {
"invalid-utf8": Buffer.concat([
Buffer.from('{"ok":false,"reason":"format","message":"'),
Buffer.from([0xff]),
Buffer.from('"}'),
]),
"literal-replacement": JSON.stringify({
ok: false,
reason: "format",
message: "valid �",
}),
"bom-reply": '\uFEFF{"ok":false,"reason":"format","message":"valid"}',
};
const encodedReply = encodedReplies[scenario];
if (encodedReply !== undefined)
await writeFile(request.reply_path, encodedReply);
else {
await writeFile(
request.reply_path,
scenario === "malformed-reply"
? "broken-json"
: JSON.stringify(
[
"format",
"unavailable",
"unsupported",
"output-limit",
].includes(scenario)
? {
ok: false,
reason: scenario,
message: "ELF table is truncated.",
}
: {
ok: true,
profile:
scenario === "wrong-profile"
? "pwntools@other"
: PWNTOOLS_PROVIDER_IDENTITY.version,
value,
},
),
);
}
return spawnOwnedProviderProcess({
...spawn,
command: process.execPath,
@@ -576,6 +597,11 @@ it
},
);
const result = await provider.inspect({ path });
if (scenario === "literal-replacement")
expect(result).toMatchObject({
ok: false,
error: { issues: [{ message: "valid �" }] },
});
if (scenario === "success") {
if (!result.ok) throw result.error;
expect(result.value.artifact).toMatchObject({
@@ -589,7 +615,7 @@ it
ok: false,
error: {
_tag:
scenario === "format"
scenario === "format" || scenario === "literal-replacement"
? "AnalysisInputError"
: scenario === "unavailable"
? "ProviderSelectionError"
@@ -381,6 +381,9 @@ it
"wrong-version",
"wrong-decoded-carrier",
"broken-reply",
"invalid-utf8",
"literal-replacement",
"bom-reply",
"out-of-range-body",
])(
"binds source carrier/worker representation and releases owned files: %s",
@@ -405,45 +408,64 @@ it
);
const hex = scenario === "wrong-decoded-carrier" ? "00" : "60006000f3";
const bytes = Buffer.from(hex, "hex");
await writeFile(
request.reply_path,
scenario === "broken-reply"
? "broken"
: JSON.stringify(
["format", "unsupported", "output-limit"].includes(scenario)
? {
ok: false,
reason: scenario,
message:
scenario === "output-limit"
? "Complete interface evidence exceeds the 16 MiB reply budget."
: "Malformed hex carrier.",
}
: {
ok: true,
version: scenario === "wrong-version" ? "0.0.0" : "0.9.3",
bytecode: {
sha256: createHash("sha256")
.update(bytes)
.digest("hex"),
bytes: bytes.length,
hex,
const encodedReplies: Partial<Record<string, string | Buffer>> = {
"invalid-utf8": Buffer.concat([
Buffer.from('{"ok":false,"reason":"format","message":"'),
Buffer.from([0xff]),
Buffer.from('"}'),
]),
"literal-replacement": JSON.stringify({
ok: false,
reason: "format",
message: "valid �",
}),
"bom-reply": '\uFEFF{"ok":false,"reason":"format","message":"valid"}',
};
const encodedReply = encodedReplies[scenario];
if (encodedReply !== undefined)
await writeFile(request.reply_path, encodedReply);
else {
await writeFile(
request.reply_path,
scenario === "broken-reply"
? "broken"
: JSON.stringify(
["format", "unsupported", "output-limit"].includes(scenario)
? {
ok: false,
reason: scenario,
message:
scenario === "output-limit"
? "Complete interface evidence exceeds the 16 MiB reply budget."
: "Malformed hex carrier.",
}
: {
ok: true,
version:
scenario === "wrong-version" ? "0.0.0" : "0.9.3",
bytecode: {
sha256: createHash("sha256")
.update(bytes)
.digest("hex"),
bytes: bytes.length,
hex,
},
raw: {
functions:
scenario === "out-of-range-body"
? [
{
selector: "aabbccdd",
bytecodeOffset: 5,
dispatch: "abi",
},
]
: [],
},
},
raw: {
functions:
scenario === "out-of-range-body"
? [
{
selector: "aabbccdd",
bytecodeOffset: 5,
dispatch: "abi",
},
]
: [],
},
},
),
);
),
);
}
return spawnOwnedProviderProcess({
...spawn,
command: process.execPath,
@@ -452,6 +474,11 @@ it
},
);
const result = await provider.inspect({ path, encoding: "hex" });
if (scenario === "literal-replacement")
expect(result).toMatchObject({
ok: false,
error: { issues: [{ message: "valid �" }] },
});
if (scenario === "success") {
if (!result.ok) throw result.error;
expect(result.value.result).toMatchObject({
@@ -465,7 +492,7 @@ it
ok: false,
error: {
_tag:
scenario === "format"
scenario === "format" || scenario === "literal-replacement"
? "AnalysisInputError"
: scenario === "unsupported"
? "AnalysisUnsupportedTargetError"
+14
View File
@@ -214,6 +214,20 @@ it("uses the pinned SDK for a local Streamable HTTP handshake, paged discovery,
).toBe(true);
});
it("retains own structured fields through the SDK before scalar-wrapper selection", async () => {
const structuredContent = {
["__proto__"]: { observed: true },
result: "ordinary field",
nested: [{ ["__proto__"]: "nested fact" }],
};
const { connection } = await fixture({
toolResult: { content: [], structuredContent },
});
await connection.connect();
const result = await connection.call("first", {});
expect(JSON.stringify(result)).toBe(JSON.stringify(structuredContent));
});
it("omits an oversized HTTP error body before retaining or redacting it", async () => {
const { connection } = await fixture({
toolStatus: 500,
@@ -0,0 +1,26 @@
import { expect, it } from "vitest";
import { createStreamableHttpJebMcpConnection } from "../../../src/jeb/JebMcpConnection.js";
import { startJebMcpFixture } from "../../fixtures/jebMcp.js";
it("retains own structured fields through the JEB SDK transport", async () => {
const expected = {
["__proto__"]: { observed: true },
success: true,
nested: [{ ["__proto__"]: "nested fact" }],
};
const fixture = await startJebMcpFixture(() => expected);
const connection = createStreamableHttpJebMcpConnection(
new URL(fixture.endpoint),
);
try {
await connection.connect();
const result = await connection.call("get_client_information", {});
expect(JSON.stringify(result)).toBe(JSON.stringify(expected));
} finally {
try {
await connection.close();
} finally {
await fixture.close();
}
}
});