mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(mcp): advertise absolute paths for tools that reject relative inputs (#9179)
* fix(mcp): advertise absolute paths for tools that reject relative inputs The domain schemas accepted relative paths, then handlers rejected them with isAbsolute. Those inputs now use absoluteLocalPathStringSchema, and the duplicate handler checks are gone. * fix(javascript): own recovery path admission in the schema Relative recovery paths now fail in absoluteLocalPathStringSchema, including the additional_paths index. Drop the duplicate handler check, and make the CLI, MCP, and tools/list tests assert that schema fact. * test: assert absolute-path facts instead of error prose Recovery and web-script tests keep the field, format code, and regex expectation. They no longer freeze the schema's explanatory sentence. --------- Co-authored-by: Cursor Agent <[email protected]>
This commit is contained in:
1 parent
923e6f77e3
commit
562779bd71
19 files changed
+180
-131
No files matched your search
@@ -1,5 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -85,21 +84,6 @@ export const exportWebScriptsValidated = async (
|
||||
resources: ArtifactResourceScope,
|
||||
options: ExecutionOptions = {},
|
||||
): Promise<Result<Evidence, AnalysisError>> => {
|
||||
const invalidPaths = (["capture_path", "output_directory"] as const).filter(
|
||||
(field) => !isAbsolute(input[field]),
|
||||
);
|
||||
if (invalidPaths.length > 0)
|
||||
return err(
|
||||
new AnalysisInputError(
|
||||
OPERATION,
|
||||
undefined,
|
||||
invalidPaths.map((field) => ({
|
||||
path: [field],
|
||||
reason: "invalid_format" as const,
|
||||
message: `${field} must be an absolute filesystem path on this host.`,
|
||||
})),
|
||||
),
|
||||
);
|
||||
try {
|
||||
if (options.signal?.aborted)
|
||||
return err(
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { isAbsolute } from "node:path";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { AppleIpsPort } from "./AppleIpsPort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
appleIpsSchema,
|
||||
@@ -38,16 +34,6 @@ export class AppleIpsService {
|
||||
options?.signal,
|
||||
);
|
||||
if (!input.ok) return input;
|
||||
if (!isAbsolute(input.value.path))
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const result = await this.provider.inspect(input.value, options);
|
||||
if (!result.ok) return result;
|
||||
const report = appleIpsSchema.safeParse(result.value);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { open, stat } from "node:fs/promises";
|
||||
import { isAbsolute } from "node:path";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import { BYTE_ENTROPY_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
@@ -105,10 +104,6 @@ export class ByteEntropyService {
|
||||
);
|
||||
if (!parsed.ok) return parsed;
|
||||
const input = parsed.value;
|
||||
if (!isAbsolute(input.path))
|
||||
return err(
|
||||
invalid("path", "Expected an absolute filesystem path on this host."),
|
||||
);
|
||||
const evidenceFromReport = (report: ByteEntropy): Evidence =>
|
||||
createEvidence(
|
||||
{ path: input.path, format: "file", sha256: report.artifact.sha256 },
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { isAbsolute } from "node:path";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { ElfNotesPort } from "./ElfNotesPort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
inspectElfNotesInputSchema,
|
||||
@@ -39,16 +35,6 @@ export class ElfNotesService {
|
||||
options?.signal,
|
||||
);
|
||||
if (!input.ok) return input;
|
||||
if (!isAbsolute(input.value.path))
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const result = await this.provider.inspect(input.value, options);
|
||||
if (!result.ok) return result;
|
||||
const report = elfNotesSchema.safeParse(result.value);
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { isAbsolute } from "node:path";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { KernelModulePort } from "./KernelModulePort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { elfObjectTypeName } from "../../domain/native/elfNotes.js";
|
||||
import {
|
||||
@@ -47,16 +43,6 @@ export class KernelModuleService {
|
||||
options?.signal,
|
||||
);
|
||||
if (!input.ok) return input;
|
||||
if (!isAbsolute(input.value.path))
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const result = await this.provider.inspect(input.value, options);
|
||||
if (!result.ok) return result;
|
||||
const report = kernelModuleSchema.safeParse(result.value);
|
||||
|
||||
@@ -154,7 +154,13 @@ describe("JavaScript recovery workflow", () => {
|
||||
expect(invoked).toBe(false);
|
||||
if (result.ok) throw new Error("expected invalid path");
|
||||
expect(result.error).toMatchObject({
|
||||
issues: [{ path: ["additional_paths"], reason: "invalid_format" }],
|
||||
issues: [
|
||||
{
|
||||
path: ["additional_paths", 0],
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { parseAnalysisInput } from "../AnalysisInput.js";
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
@@ -38,27 +34,6 @@ export class JavaScriptRecoveryService {
|
||||
options?.signal,
|
||||
);
|
||||
if (!parsed.ok) return parsed;
|
||||
const invalidPaths = (["path", "output_directory"] as const).filter(
|
||||
(field) => !isAbsolute(parsed.value[field]),
|
||||
);
|
||||
const invalidAdditional = (parsed.value.additional_paths ?? []).filter(
|
||||
(additional) => !isAbsolute(additional),
|
||||
);
|
||||
if (invalidPaths.length > 0 || invalidAdditional.length > 0)
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
...invalidPaths.map((field) => ({
|
||||
path: [field],
|
||||
reason: "invalid_format" as const,
|
||||
message: `${field} must be an absolute filesystem path on this host.`,
|
||||
})),
|
||||
...invalidAdditional.map((additional) => ({
|
||||
path: ["additional_paths"],
|
||||
reason: "invalid_format" as const,
|
||||
message: `additional_paths entries must be absolute filesystem paths on this host: ${additional}`,
|
||||
})),
|
||||
]),
|
||||
);
|
||||
const execution = await this.provider.recover(parsed.value, options);
|
||||
if (!execution.ok) return execution;
|
||||
const result = javascriptRecoveryResultSchema.safeParse(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Cli, z } from "incur";
|
||||
|
||||
import { exportWebScripts } from "../application/WebScriptExportService.js";
|
||||
import { exportWebScriptsInputSchema } from "../domain/webScriptExport.js";
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
@@ -18,14 +19,8 @@ export const registerWebScriptCommands = (
|
||||
description:
|
||||
"Export retained website scripts into an absent directory for static JavaScript analysis",
|
||||
args: z.object({
|
||||
capturePath: z
|
||||
.string()
|
||||
.describe("Absolute path to saved page or scenario capture JSON"),
|
||||
outputDirectory: z
|
||||
.string()
|
||||
.describe(
|
||||
"Absolute absent directory for verified scripts and manifest",
|
||||
),
|
||||
capturePath: exportWebScriptsInputSchema.shape.capture_path,
|
||||
outputDirectory: exportWebScriptsInputSchema.shape.output_directory,
|
||||
}),
|
||||
run: ({ args }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { z } from "zod";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { analyzeJavaScriptApplicationInputSchema } from "./javascriptApplicationAnalysis.js";
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
import { absoluteLocalPathStringSchema } from "../localPath.js";
|
||||
|
||||
/** Recover derived readable modules from explicitly selected local scripts. */
|
||||
export const javascriptRecoveryInputSchema = z.strictObject({
|
||||
path: localPathStringSchema.describe(
|
||||
path: absoluteLocalPathStringSchema.describe(
|
||||
"Absolute path to a UTF-8 JavaScript file; never executed",
|
||||
),
|
||||
additional_paths: z
|
||||
.array(localPathStringSchema)
|
||||
.array(absoluteLocalPathStringSchema)
|
||||
.max(16)
|
||||
.optional()
|
||||
.describe(
|
||||
"Additional absolute UTF-8 JavaScript files recovered together with path; each is snapshotted separately so imports can resolve across related chunks. All inputs share a 64 MiB total budget",
|
||||
),
|
||||
output_directory: localPathStringSchema.describe(
|
||||
output_directory: absoluteLocalPathStringSchema.describe(
|
||||
"Absolute absent output directory; parent must exist",
|
||||
),
|
||||
extraction_mode: z
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
import { absoluteLocalPathStringSchema } from "../localPath.js";
|
||||
|
||||
const uint = z.number().int().min(0).max(0xffffffff);
|
||||
const recordedIntegerSchema = z.strictObject({
|
||||
@@ -52,7 +52,7 @@ export const APPLE_IPS_IMAGE_SOURCE_MEANING: Readonly<
|
||||
|
||||
/** Complete bounded inspection of one Apple .ips crash report. */
|
||||
export const inspectAppleIpsInputSchema = z.strictObject({
|
||||
path: localPathStringSchema.describe(
|
||||
path: absoluteLocalPathStringSchema.describe(
|
||||
"Absolute filesystem path to an Apple .ips crash report",
|
||||
),
|
||||
max_file_bytes: z
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
import { absoluteLocalPathStringSchema } from "../localPath.js";
|
||||
|
||||
const offsetSchema = z
|
||||
.number()
|
||||
@@ -10,7 +10,9 @@ const entropySchema = z.number().min(0).max(8);
|
||||
|
||||
/** Select blocks relative to an explicit byte window; identity remains file-wide. */
|
||||
export const inspectByteEntropyInputSchema = z.strictObject({
|
||||
path: localPathStringSchema,
|
||||
path: absoluteLocalPathStringSchema.describe(
|
||||
"Absolute filesystem path to the file",
|
||||
),
|
||||
block_size: z
|
||||
.number()
|
||||
.int()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
import { absoluteLocalPathStringSchema } from "../localPath.js";
|
||||
|
||||
const uint = z.number().int().min(0).max(0xffffffff);
|
||||
const range = z.strictObject({ offset: uint, bytes: uint });
|
||||
@@ -86,7 +86,7 @@ export const coreNoteTypeName = (type: number): ElfNoteTypeName => {
|
||||
|
||||
/** Complete bounded inspection of ELF PT_NOTE and SHT_NOTE tables. */
|
||||
export const inspectElfNotesInputSchema = z.strictObject({
|
||||
path: localPathStringSchema.describe(
|
||||
path: absoluteLocalPathStringSchema.describe(
|
||||
"Absolute filesystem path to an ELF image",
|
||||
),
|
||||
max_file_bytes: z
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { fixedTupleSchema } from "../fixedTupleSchema.js";
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
import { absoluteLocalPathStringSchema } from "../localPath.js";
|
||||
import { ELF_OBJECT_TYPE_NAMES } from "./elfNotes.js";
|
||||
|
||||
const uint = z.number().int().min(0).max(0xffffffff);
|
||||
@@ -197,7 +197,7 @@ const signatureSchema = z.strictObject({
|
||||
|
||||
/** Complete bounded inspection of one relocatable Linux kernel module. */
|
||||
export const inspectKernelModuleInputSchema = z.strictObject({
|
||||
path: localPathStringSchema.describe(
|
||||
path: absoluteLocalPathStringSchema.describe(
|
||||
"Absolute filesystem path to an ELF kernel module or a compressed .ko container",
|
||||
),
|
||||
max_file_bytes: z
|
||||
|
||||
@@ -3,14 +3,14 @@ import { z } from "zod";
|
||||
import { browserCompletenessSchema } from "./browserCompleteness.js";
|
||||
import { browserScenarioCompletenessSchema } from "./browserScenarioCaptureValues.js";
|
||||
import { digestSchema, prefixedDigestSchema } from "./digests.js";
|
||||
import { localPathStringSchema } from "./localPath.js";
|
||||
import { absoluteLocalPathStringSchema } from "./localPath.js";
|
||||
|
||||
/** Export already captured scripts into one absent, caller-selected directory. */
|
||||
export const exportWebScriptsInputSchema = z.strictObject({
|
||||
capture_path: localPathStringSchema.describe(
|
||||
capture_path: absoluteLocalPathStringSchema.describe(
|
||||
"Absolute filesystem path to saved inspect_web_page or capture_browser_scenario JSON, normalized result or complete Evidence",
|
||||
),
|
||||
output_directory: localPathStringSchema.describe(
|
||||
output_directory: absoluteLocalPathStringSchema.describe(
|
||||
"Absolute absent filesystem directory for verified scripts and manifest; parent directory must exist",
|
||||
),
|
||||
});
|
||||
|
||||
@@ -261,11 +261,6 @@ const recoveryInputLayoutPath = async (
|
||||
path: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> => {
|
||||
if (!isAbsolute(path))
|
||||
throw recoveryInputFailure(
|
||||
"path",
|
||||
"Select an absolute JavaScript file path",
|
||||
);
|
||||
try {
|
||||
checkRecoveryCancellation(signal);
|
||||
const parent = dirname(path);
|
||||
|
||||
@@ -241,14 +241,34 @@ cliTest(
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(response.isError).toBe(true);
|
||||
expect(parseMcpToolError(response)).toEqual({ error: result.json });
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: "recover_javascript_sources",
|
||||
issues: [{ path: [field], reason: "invalid_format" }],
|
||||
code: "VALIDATION_ERROR",
|
||||
fieldErrors: [
|
||||
expect.objectContaining({
|
||||
path: field === "path" ? "path" : "outputDirectory",
|
||||
code: "invalid_format",
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(parseMcpToolError(response)).toMatchObject({
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: "recover_javascript_sources",
|
||||
issues: [
|
||||
{
|
||||
path: [field],
|
||||
reason: "invalid_format",
|
||||
expected: "regex",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(result.json)).not.toContain("REA_WAKARU_COMMAND");
|
||||
expect(JSON.stringify(parseMcpToolError(response))).not.toContain(
|
||||
"REA_WAKARU_COMMAND",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -158,7 +158,11 @@ describe("CLI advertised input schemas", () => {
|
||||
const validate = ajv.compile(schema.options ?? {});
|
||||
for (const count of [16, 17]) {
|
||||
const input = {
|
||||
[field]: Array.from({ length: count }, () => "./selected"),
|
||||
[field]: Array.from({ length: count }, () =>
|
||||
command === "recover-javascript-sources"
|
||||
? "/selected"
|
||||
: "./selected",
|
||||
),
|
||||
};
|
||||
expect(validate(input)).toBe(count === 16);
|
||||
expect(
|
||||
|
||||
@@ -795,8 +795,7 @@ describe("captured script path validation", () => {
|
||||
{
|
||||
path: ["capture_path"],
|
||||
reason: "invalid_format",
|
||||
message:
|
||||
"capture_path must be an absolute filesystem path on this host.",
|
||||
expected: "regex",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -815,8 +814,7 @@ describe("captured script path validation", () => {
|
||||
{
|
||||
path: ["output_directory"],
|
||||
reason: "invalid_format",
|
||||
message:
|
||||
"output_directory must be an absolute filesystem path on this host.",
|
||||
expected: "regex",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import type { AnySchema } from "ajv";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { JAVASCRIPT_RECOVERY_TOOL_CONTRACTS } from "../../../src/contracts/javascript/javascriptRecoveryToolContracts.js";
|
||||
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "../../../src/contracts/native/binaryDiagnosticsToolContracts.js";
|
||||
import { WEB_SCRIPT_TOOL_CONTRACTS } from "../../../src/contracts/webScriptToolContracts.js";
|
||||
import { RequestTrackingMcpServer } from "../../../src/server/RequestTrackingMcpServer.js";
|
||||
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
|
||||
|
||||
const CONTRACTS = [
|
||||
...BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
|
||||
...WEB_SCRIPT_TOOL_CONTRACTS,
|
||||
...JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
|
||||
];
|
||||
|
||||
const PATH_FIELDS = {
|
||||
inspect_apple_ips: ["path"],
|
||||
inspect_elf_notes: ["path"],
|
||||
inspect_kernel_module: ["path"],
|
||||
inspect_byte_entropy: ["path"],
|
||||
export_web_scripts: ["capture_path", "output_directory"],
|
||||
recover_javascript_sources: ["path", "output_directory", "additional_paths"],
|
||||
} as const;
|
||||
|
||||
const absoluteFile =
|
||||
process.platform === "win32" ? "C:\\sample.bin" : "/tmp/sample.bin";
|
||||
const absoluteDirectory = process.platform === "win32" ? "C:\\out" : "/tmp/out";
|
||||
|
||||
const contractFor = (name: keyof typeof PATH_FIELDS) => {
|
||||
const contract = CONTRACTS.find((candidate) => candidate.name === name);
|
||||
if (contract === undefined) throw new Error(`Missing contract ${name}`);
|
||||
return contract;
|
||||
};
|
||||
|
||||
const validInput = (
|
||||
name: keyof typeof PATH_FIELDS,
|
||||
): Record<string, unknown> => {
|
||||
if (name === "export_web_scripts")
|
||||
return {
|
||||
capture_path: absoluteFile,
|
||||
output_directory: absoluteDirectory,
|
||||
};
|
||||
if (name === "recover_javascript_sources")
|
||||
return {
|
||||
path: absoluteFile,
|
||||
output_directory: absoluteDirectory,
|
||||
additional_paths: [absoluteFile],
|
||||
};
|
||||
return { path: absoluteFile };
|
||||
};
|
||||
|
||||
const withRelativeField = (
|
||||
name: keyof typeof PATH_FIELDS,
|
||||
field: string,
|
||||
): Record<string, unknown> => ({
|
||||
...validInput(name),
|
||||
[field]: field === "additional_paths" ? ["sample.bin"] : "sample.bin",
|
||||
});
|
||||
|
||||
it("rejects one relative path field at a time on tools/list", async () => {
|
||||
const server = new RequestTrackingMcpServer({
|
||||
name: "absolute-path-schema",
|
||||
version: "1",
|
||||
});
|
||||
for (const name of Object.keys(PATH_FIELDS) as (keyof typeof PATH_FIELDS)[]) {
|
||||
const contract = contractFor(name);
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
() => ({ content: [] }),
|
||||
);
|
||||
}
|
||||
const client = new Client({ name: "absolute-path-client", version: "1" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const checked: string[] = [];
|
||||
try {
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const listed = await client.listTools();
|
||||
for (const name of Object.keys(
|
||||
PATH_FIELDS,
|
||||
) as (keyof typeof PATH_FIELDS)[]) {
|
||||
const schema = listed.tools.find(
|
||||
(tool) => tool.name === name,
|
||||
)?.inputSchema;
|
||||
expect(schema, name).toBeDefined();
|
||||
if (schema === undefined) throw new Error(`Missing schema ${name}`);
|
||||
const validate = ajv.compile(schema as AnySchema);
|
||||
expect(validate(validInput(name)), `${name} valid`).toBe(true);
|
||||
for (const field of PATH_FIELDS[name]) {
|
||||
expect(
|
||||
validate(withRelativeField(name, field)),
|
||||
`${name}.${field}`,
|
||||
).toBe(false);
|
||||
checked.push(`${name}.${field}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await client.close();
|
||||
await server.close();
|
||||
}
|
||||
expect(checked).toEqual([
|
||||
"inspect_apple_ips.path",
|
||||
"inspect_elf_notes.path",
|
||||
"inspect_kernel_module.path",
|
||||
"inspect_byte_entropy.path",
|
||||
"export_web_scripts.capture_path",
|
||||
"export_web_scripts.output_directory",
|
||||
"recover_javascript_sources.path",
|
||||
"recover_javascript_sources.output_directory",
|
||||
"recover_javascript_sources.additional_paths",
|
||||
]);
|
||||
});
|
||||
Reference in new issue
Block a user