Files
reverse-engineering-anything/src/catalogDiscovery.ts
T
gensokyo-dev bacc1e3846 perf(mcp)!: defer retained views in progressive catalogs (#9151)
Keep full schemas and canonical analysis unchanged while loading the cross-domain view through existing named discovery. Qualify real recent-tool workflows with token ledgers, latency, CPU, family RSS and retained-follow-up controls.

BREAKING CHANGE: The opt-in hot-cold catalog no longer preloads inspect_analysis_view. Discover it before calling it, batch with its producer when needed, or use the unchanged default full profile.
2026-11-06 22:44:56 +08:00

120 lines
3.9 KiB
TypeScript

import { createHash } from "node:crypto";
import { z } from "zod";
import type { createCatalogIdentity } from "./catalogIdentity.js";
// Retained views are loaded like producers: their full cross-domain output
// contract is unnecessary until a task actually queries retained analysis.
export const HOT_TOOL_NAMES: ReadonlySet<string> = new Set([
"binary_session",
"open_binary",
"close_binary",
"get_evidence_bundle",
]);
// One page activates at most 20 schemas: callers can page a broad search instead
// of inadvertently expanding the entire catalog into their client's context.
const MAX_DISCOVERY_TOOLS = 20;
const DEFAULT_DISCOVERY_PAGE_SIZE = 3;
const pageSize = z
.int()
.min(1)
.max(MAX_DISCOVERY_TOOLS)
.default(DEFAULT_DISCOVERY_PAGE_SIZE);
const inputSchema = z
.strictObject({
query: z
.string()
.trim()
.optional()
.describe(
"Case-insensitive words matched against tool names and descriptions; all words must match. Omit to browse.",
),
names: z
.array(z.string().min(1))
.min(1)
.max(MAX_DISCOVERY_TOOLS)
.optional()
.describe(
`At most ${MAX_DISCOVERY_TOOLS} exact tool names to load instead of searching.`,
),
offset: z.int().min(0).default(0),
limit: pageSize.describe(
`Search page size 1..${MAX_DISCOVERY_TOOLS} (default ${DEFAULT_DISCOVERY_PAGE_SIZE}); does not apply to exact names.`,
),
})
.refine(
(input) =>
input.names === undefined ||
(input.query === undefined && input.offset === 0),
{
message: "Use names alone, or query with offset/limit.",
},
);
const outputSchema = z.strictObject({
profile: z.literal("hot-cold"),
inventory_count: z.int().nonnegative(),
loaded_count: z.int().nonnegative(),
total_matches: z.int().nonnegative(),
next_offset: z.int().nonnegative().nullable(),
tools: z.array(
z.strictObject({
name: z.string(),
description: z
.string()
.describe(
"Abbreviated canonical description; read tools/list for the complete contract.",
),
input_schema: z
.record(z.string(), z.unknown())
.describe(
"The configured advertised input schema for calling this named tool.",
),
}),
),
next_step: z.string(),
});
export const DISCOVER_TOOLS = {
name: "discover_tools",
description:
"Find and load REA tools on demand. Search by task keywords (for example javascript, sqlite, strings, imports), or supply exact names. Returned tools become callable with their original names and full schemas after your client refreshes tools/list. Loaded tools remain available for this connection. This only changes catalog visibility; it does not run analysis or grant execution authority. If tools do not appear, enable MCP catalog refresh in your client or use REA_MCP_TOOL_CATALOG_PROFILE=full.",
inputSchema,
outputSchema,
annotations: {
readOnlyHint: true,
idempotentHint: true,
openWorldHint: false,
},
} as const;
export type CatalogIdentity = ReturnType<typeof createCatalogIdentity>;
const digest = (value: unknown): string =>
createHash("sha256").update(JSON.stringify(value)).digest("hex");
/** Identity covers the discoverable inventory and policy, not mutable visibility. */
export const progressiveCatalogIdentity = (
base: CatalogIdentity,
): CatalogIdentity => {
const policy = {
profile: "hot-cold",
hot: [...HOT_TOOL_NAMES],
tool: {
...DISCOVER_TOOLS,
inputSchema: z.toJSONSchema(inputSchema, { io: "input" }),
outputSchema: z.toJSONSchema(outputSchema),
},
};
const tools_sha256 = digest({ base: base.digests.tools_sha256, policy });
return {
counts: { ...base.counts, mcp_tools: base.counts.mcp_tools + 1 },
digests: {
tools_sha256,
prompts_sha256: base.digests.prompts_sha256,
combined_sha256: digest({
base: base.digests.combined_sha256,
tools_sha256,
}),
},
};
};