Compare commits

...
19 changed files with 1643 additions and 89 deletions

No files matched your search

+28 -1
View File
@@ -1996,6 +1996,26 @@ check full-profile local-path rules. Compact MCP workflow tests verify canonical
rejection of invalid inputs before provider work, independent of how much detail
the selected advertisement retains.
### Retained-view guidance
`inspect_analysis_view` advertises the shared source, Evidence and paging rules.
Its format-specific selectors are available on demand through the existing
guidance tools: search with `{"query":"inspect_analysis_view <format or view kind>"}`,
then pass a returned locator to `read_guidance_section`. For example, search for
`inspect_analysis_view OpenAPI` before selecting retained API operations.
These compiled tool sections ship in the runtime alongside the installed workflow
guides; they use the same bundle digest, section selection and stale-locator checks.
They are documentation, not target observations or Evidence. CLI callers use
`rea guidance --action search --query "inspect_analysis_view OpenAPI"`, then
`rea guidance --action read --locator <locator>` through the same owner.
In hot/cold catalogs, first discover `search_guidance` and `read_guidance_section`
by their exact names. Keyword tool discovery also searches the compiled tool
guidance, so moving a selector out of the advertised description does not remove
its tool-discovery keywords. Full input/output schemas and canonical validation
remain unchanged; a full-catalog client does not need catalog refresh to use the
guidance tools.
<a id="hotcold-catalog-discovery"></a>
### Hot/cold catalog discovery
@@ -2005,6 +2025,13 @@ clients that refresh `tools/list` after `notifications/tools/list_changed`.
The default, `full`, retains the complete upfront advertisement. Restart the
server to change profiles. See [client configuration](../getting-started/client-configuration.md#hotcold-catalogs).
Legacy connections receive list-change notifications unsolicited. On protocol
revision `2026-07-28`, subscribe to tool-list changes with `subscriptions/listen`;
the pinned JS SDK exposes `client.listen({toolsListChanged: true})` or the
`ClientOptions.listChanged.tools` convenience handler. Registering a notification
handler alone does not open a modern subscription. Refresh `tools/list` after
the subscribed notification before calling newly loaded tools.
Hot/cold initially advertises `binary_session`, `open_binary`, `close_binary`,
`get_evidence_bundle`, and the adapter's `discover_tools`.
`inspect_analysis_view` is loaded on demand like other analysis tools. When a
@@ -2014,7 +2041,7 @@ that accept `view` can answer a selected first question without loading the
cross-domain view contract. Complete output schemas and canonical result
validation are preserved when each tool loads.
An explicit tool-family scope can remove any of those domain tools.
`discover_tools` searches canonical names and descriptions using case-insensitive
`discover_tools` searches canonical names, descriptions and compiled tool guidance using case-insensitive
words, or accepts exact `names`. It loads the matching page and returns names
and abbreviated descriptions with their configured input schemas, so the next
call can use the returned arguments contract. Output schemas remain in
+14 -1
View File
@@ -17,6 +17,7 @@ const [
{ PROMPT_CONTRACTS },
{ TOOL_CONTRACTS },
{ getEvidenceInputContract },
{ toolOutputSchemaWithMetadata },
] = await Promise.all([
read("server/createServer.js"),
read("composition/binary.js"),
@@ -25,6 +26,7 @@ const [
read("contracts/promptContracts.js"),
read("contracts/toolContracts.js"),
read("domain/evidenceInputContract.js"),
read("contracts/toolSchemaMetadata.js"),
]);
const recordCatalog = async (profile) => {
// Catalog generation is independent of the host account and user configuration.
@@ -59,7 +61,18 @@ const recordCatalog = async (profile) => {
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const tools = (await client.listTools()).tools;
const tools = (await client.listTools()).tools.map((tool) => {
const contract = TOOL_CONTRACTS.find(({ name }) => name === tool.name);
if (contract === undefined) throw new Error(`Missing tool ${tool.name}`);
return {
...tool,
// Record the pre-codec schema. The SDK owns protocol-specific root
// wrapping for both tools/list and tools/call on the deferred server.
outputSchema: toolOutputSchemaWithMetadata(contract)[
"~standard"
].jsonSchema.output({ target: "draft-2020-12" }),
};
});
const prompts = (await client.listPrompts()).prompts.map((prompt) => {
const contract = PROMPT_CONTRACTS.find(
(candidate) => candidate.name === prompt.name,
@@ -2,6 +2,8 @@ import { expect, it } from "vitest";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { readGuidanceSectionInputSchema } from "../contracts/guidanceContracts.js";
import { MANAGED_SKILL_FILES } from "./SetupSkill.js";
import { TOOL_GUIDANCE } from "../toolGuidance.js";
import { parseGuidanceSections } from "./GuidanceSections.js";
import {
readGuidanceSectionWithReader,
searchGuidanceWithReader,
@@ -76,21 +78,33 @@ it("issues readable locators independently of heading length", async () => {
await resources.close();
});
it.each([2048, 2049])(
"bounds the combined index across guides with %i sections each",
it.each([4096, 4097])(
"bounds the combined skill and tool index at %i sections",
async (count) => {
const resources = new ArtifactResourceScope();
const compiled = parseGuidanceSections(
new Map(
Object.values(TOOL_GUIDANCE).map(({ guide, content }) => [
guide,
content,
]),
),
);
if (compiled === undefined) throw new Error("Invalid compiled guidance");
const remaining = count - compiled.length;
const result = await searchGuidanceWithReader(
{ query: "needle", limit: 1 },
async (path) =>
path === MANAGED_SKILL_FILES[0] || path === MANAGED_SKILL_FILES[1]
? "# needle\n".repeat(count)
: "",
path === MANAGED_SKILL_FILES[0]
? "# needle\n".repeat(Math.floor(remaining / 2))
: path === MANAGED_SKILL_FILES[1]
? "# needle\n".repeat(Math.ceil(remaining / 2))
: "",
resources,
);
expect(result).toMatchObject({
ok: true,
value: { status: count === 2048 ? "matches" : "bundle_unavailable" },
value: { status: count === 4096 ? "matches" : "bundle_unavailable" },
});
await resources.close();
},
+14 -3
View File
@@ -5,6 +5,7 @@ import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import { MANAGED_SKILL_FILES, readSkillFile } from "./SetupSkill.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import { TOOL_GUIDANCE } from "../toolGuidance.js";
import {
MAX_SUBSECTION_POINTERS,
readGuidanceSectionInputSchema,
@@ -32,6 +33,13 @@ const SKILL_ROOT = `../../skills/${PRODUCT_IDENTITY.skillName}/`;
const SOURCE = "installed_rea_guidance" as const;
const MAX_GUIDE_FILE_BYTES = 1_048_576;
const MAX_GUIDE_BUNDLE_BYTES = 4_194_304;
const COMPILED_GUIDES = new Map(
Object.values(TOOL_GUIDANCE).map(({ guide, content }) => [guide, content]),
);
const GUIDANCE_FILES: ReadonlySet<string> = new Set([
...MANAGED_SKILL_FILES,
...COMPILED_GUIDES.keys(),
]);
const sha256 = (value: string | Uint8Array): string =>
createHash("sha256").update(value).digest("hex");
@@ -85,8 +93,11 @@ const readBundle = async (
resources: ArtifactResourceScope,
operation: string,
): Promise<GuidanceBundleRead> => {
const files = new Map<string, string>();
let bundleBytes = 0;
const files = new Map(COMPILED_GUIDES);
let bundleBytes = [...files.values()].reduce(
(bytes, content) => bytes + Buffer.byteLength(content, "utf8"),
0,
);
try {
for (const relativePath of MANAGED_SKILL_FILES) {
const content = await readGuide(relativePath);
@@ -712,7 +723,7 @@ const decodeLocator = (locator: string): LocatorIdentity | undefined => {
typeof item.bundle !== "string" ||
!/^[a-f0-9]{64}$/u.test(item.bundle) ||
typeof item.guide !== "string" ||
!(MANAGED_SKILL_FILES as readonly string[]).includes(item.guide) ||
!GUIDANCE_FILES.has(item.guide) ||
!Number.isInteger(item.ordinal) ||
typeof item.section !== "string" ||
!/^[a-f0-9]{64}$/u.test(item.section)
+3 -1
View File
@@ -1,6 +1,7 @@
import { createHash } from "node:crypto";
import { z } from "zod";
import type { createCatalogIdentity } from "./catalogIdentity.js";
import { TOOL_GUIDANCE } from "./toolGuidance.js";
// Retained views are loaded like producers: their full cross-domain output
// contract is unnecessary until a task actually queries retained analysis.
@@ -27,7 +28,7 @@ const inputSchema = z
.trim()
.optional()
.describe(
"Case-insensitive words matched against tool names and descriptions; all words must match. Omit to browse.",
"Case-insensitive words matched against tool names, descriptions and bundled tool guidance; all words must match. Omit to browse.",
),
names: z
.array(z.string().min(1))
@@ -98,6 +99,7 @@ export const progressiveCatalogIdentity = (
const policy = {
profile: "hot-cold",
hot: [...HOT_TOOL_NAMES],
guidance: TOOL_GUIDANCE,
tool: {
...DISCOVER_TOOLS,
inputSchema: z.toJSONSchema(inputSchema, { io: "input" }),
+1 -1
View File
@@ -49,7 +49,7 @@ export const registerGuidanceCommands = (
resources: CliGuidanceResources = processGuidanceResources,
): void => {
cli.command(CLI_COMMANDS.guidance, {
description: "Search or read the installed REA workflow guidance",
description: "Search or read the installed REA workflow and tool guidance",
args: z.object({}),
options: z
.object({
+379
View File
@@ -0,0 +1,379 @@
/** Format-specific documentation, indexed on demand rather than advertised with every tool. */
export const ANALYSIS_VIEW_GUIDANCE = `# Retained analysis view guidance
Each section documents \`inspect_analysis_view\` for a compatible completed parent.
Use \`search_guidance\` with \`inspect_analysis_view\` and a view kind, then read the
returned locator with \`read_guidance_section\`. These are instructions, not Evidence.
## inspect_analysis_view: Native procedure references
Use native-procedure-references to page retained procedure reference or unresolved-call
rows. Endpoint kind selected-procedure refers to the summary procedure, null preserves
the original null, and shared carries an index into items.endpoints, where each other
procedure is stored once per page. Exact source_address and target_address filters apply
to reference rows.
## inspect_analysis_view: OpenAPI and GraphQL contracts
For inspect_api_contract OpenAPI parents, use kind api-contract, collection operations,
with exact methods/paths, selected fields and matching-row pagination. For example:
view {kind: "api-contract", collection: "operations", paths: ["/items"],
fields: ["method", "path"], offset: 0, limit: 20}. This reads retained declarations;
page exhaustion does not establish runtime endpoint behavior.
For inspect_api_contract parents, graphql-contract pages retained SDL types/fields,
operation definitions, field references, fragment-spreads or diagnostics with exact
filters and selected columns.
## inspect_analysis_view: JavaScript routes
Use javascript-routes collections to query route patterns, source-linked occurrences,
source status, unknowns or summary counts from retained JavaScript application or
web-bundle Evidence.
## inspect_analysis_view: Web bundle endpoints
Use web-bundle collections endpoints or inferences to page endpoint observations and
inferences from retained analyze_web_bundle Evidence; summary returns target URL,
origin, and counts.
## inspect_analysis_view: Native feature traces
For retained trace_feature Evidence, kind native-feature-trace pages matches, references
or unknowns; matches filter exact address, references filter exact source_address and
target_address, source_index identifies each original row, and list_unknowns pages
durable question heads.
## inspect_analysis_view: Recorded arrays and pseudocode
Inspect selected views of supported completed Evidence without repeating analysis. Use
recorded-array with an exact raw_result object-member path to page a provider-recorded
array when the producer supplies that path; rows keep their raw meaning and the parent
retains coverage. For procedure_pseudo_code Evidence, summary reports text availability
and the original selection; kind native with facet pseudocode pages the original UTF-16
text. Other native facets require completed analyze_function Evidence. Select a view
compatible with the original parent operation; inspect_analysis_view does not rerun it.
## inspect_analysis_view: Historical command and capture records
Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once,
source parameters, completion facts and parent identity. They do not establish current
target state. Historical capture summaries return identity and counts without bodies or
archive inventories. Capture views page retained records: view {kind: "capture", detail:
"preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields
and binary descriptors, complete returns exact records with all sidecars. Offsets follow
the selected parent record order, preserving original producer ordinals. Paging never
reopens the source or reruns the decoder.
## inspect_analysis_view: Native export tables
Native export tables use kind native-exports, collection exports/issues, exact names or
original indices, selected fields and matching-row pagination; parent Evidence plus
source_index identifies each row. Source coverage stays separate from page coverage.
## inspect_analysis_view: Erlang BEAM
BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact
names/indices/fields and matching-row pages; import modules intersects function names.
source_index identifies original parent-and-collection rows, preserving every arity and
raw atom index. beam.counts preserves null missing tables versus decoded empty tables;
runtime absence remains unknown.
## inspect_analysis_view: Python PYC
PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging;
source_index scopes every same-name alternative to the parent, nested_code keeps
code-constant references, and pyc.coverage preserves source coverage.
## inspect_analysis_view: Go metadata
Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices,
fields and pagination. Row index refers to the original collection; empty selections
return no rows, and dependency coverage remains unknown.
## inspect_analysis_view: Rust dependencies
Rust metadata uses kind rust, collection packages/edges, exact names/versions or
original indices, selected fields, edge from_indices/to_indices and matching-row
pagination. Embedded-record coverage is separate from unknown runtime dependencies.
## inspect_analysis_view: PCAP captures
For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni,
protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses,
or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained
facts; source coverage remains explicit.
## inspect_analysis_view: Qlog traces
Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams,
loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except
traces, select trace_index; stream-specific facets also require exact stream_id and
direction. Facts retain all-recorded-events-before-selection scope, and interval overlap
or endpoint loss declarations do not prove retransmission or network loss.
## inspect_analysis_view: API surface
The api-surface view groups retained inspect_web_network_capture HAR requests by method,
origin and format-rule path
template; templated segments are inferences. It reads no headers or bodies and reports
no query values.
## inspect_analysis_view: Environment manifest
The environment-manifest view projects retained inspect_web_network_capture HAR requests
on the primary origin (most
JSON-like records) into a replayable tool surface: view {kind: "environment-manifest",
template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"],
include_query_parameter_names: true}. Each tool has a deterministic name, a path
template and parameters from the four format rules plus the selected opt-in rules,
observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and
content-type sets and record ordinals; records are pointers into the parent, excluded
groups carry a reason and three fixed unknowns are always stated. Templates, parameters
and names are inferences; the view copies no header, cookie or body values and refuses
HAR parents whose sensitivity declarations are missing or record sensitive values.
Do not remove sensitivity declarations to bypass this refusal.
For capture_browser_scenario Evidence, environment-manifest requires
capture.network.response_body or header_values to have been selected. It projects one
record per network transaction in request order, relies on capture-time redaction of
declared secrets, and leaves a transaction whose URL was redacted unrepresented.
## inspect_analysis_view: Browser scenario captures
Retained capture_browser_scenario Evidence supports summary and browser-scenario
steps/events/transactions/text pages, preserving step_indices, event_kinds,
transaction_ids and selected artifacts. For an environment-manifest projection, search
for "inspect_analysis_view Environment manifest" and read its section; that projection
has separate capture selection and redaction requirements.
## inspect_analysis_view: Environment state
The environment-state view extracts initial state from retained inspect_web_network_capture
HAR or capture_browser_scenario Evidence. Use view {kind: "environment-state",
template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"]}, selecting
the same optional template_rules as the accompanying environment-manifest. It reads
response bodies recorded before the
first related write and reports entity types with an identity field (proved by a path
parameter or inferred from a field name), entities in collection order with per-view
values where views differ, documents for non-parameterized reads that are not entities
and have one initial request line (item-shaped paths stay recorded), and a render plan
per read tool: item and list views reproduce their latest recordings from the state (a
list also learns the filters, search, sort and paging its recordings verify, and answers
new values of them from the state), and every other read tool is a recorded view with a
reason. It also reports a write plan per state-changing tool (create, update, delete or
document, built from its first successful call) and write_clock; a plan is served only
when replaying the capture shows a later read confirming its effect and none
contradicting it. Types and identity are inferences; values are recorded data, so it
refuses HAR parents whose sensitivity declarations are missing or record sensitive
values. Do not remove sensitivity declarations to bypass this refusal. A scenario
parent must have selected capture.network.response_body; its values keep the capture
redaction markers, and a write
whose URL it redacted ends the state of every type.
## inspect_analysis_view: Environment coverage
The environment-coverage view summarizes initial-state projection from retained
inspect_web_network_capture HAR or capture_browser_scenario Evidence without returning
its entities. Use view {kind: "environment-coverage", template_rules:
["after-uid-literal", "opaque-token", "sibling-variance"]}, selecting the same optional
template_rules as the accompanying environment-manifest and environment-state views.
reads.state and reads.recorded count
read tools answered from the state and replayed (recorded_reasons per view reason),
writes.served lists the served write tools, and writes.withheld lists each other write
tool with its reason, plan record, contradicting read and a hint about what to record
next. It refuses HAR parents whose sensitivity declarations are missing or record
sensitive values; do not remove sensitivity declarations to bypass this refusal.
A scenario parent must have selected capture.network.response_body, and retained
capture-time redaction markers remain authoritative. For state modelling and write
confirmation rules, search for "inspect_analysis_view Environment state" and read
its section.
## inspect_analysis_view: Sources and summaries
For a retained record use source: {kind: "retained-evidence", evidence_id:
"<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary
receipt. Source kind is required. Portable source uses {kind: "inline", evidence:
<complete Evidence>}. A completed Evidence JSON file on this host uses {kind:
"evidence-file", path: "<absolute path>"}: REA reads and authenticates it, then projects
it like retained Evidence, so a large capture is bounded by the file reader rather than
by inline-input accounting. The file may also be an Evidence bundle written by CLI
--evidence_output or export_evidence_bundle; select evidence_id when it contains more
than one record. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder,
EVM, managed artifact, Android package and Dart AOT sources, request a summary with
view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives
validation, counts, WAT digest/length, tool profile and execution/network states without
reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module,
or a stable page.
## inspect_analysis_view: JavaScript modules and facts
For an exact module's retained facts and source locations, search guidance for
"read module without repeating analysis" and follow that retained-parent workflow.
Module pages include assets, bundled and source modules; use node_id for ambiguous or
unavailable paths. Typed module references use view {kind: "javascript", collection:
"modules", projection: "references", where: {path: "main.js", kind:
"javascript-module"}, offset: 0, limit: 64}; where matches exact retained strings (null
selects unavailable paths). Rows include canonical identity, retained source-range count
and observation support states. Source coverage and selection remain explicit;
exhausting matching rows does not prove runtime loading or complete analysis. For exact
file/native facts and processing gaps, use view {kind: "javascript-facts", where:
{paths: ["package.json"]}, property_keys: ["name", "operation", "analysis_status"]}.
Rows page original observations with canonical identities and Evidence; missing
properties remain omitted. Paths match recorded artifact-path/source-range locations;
optional kinds filter nodes. To discover field names without values, opt into
include_available_property_keys: true with property_keys: []. Canonical identity and
Evidence are separate from properties; keep each node_id/observation_id pair together
when composing results.
## inspect_analysis_view: Artifact inventory
Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths:
["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges
or integrity contradictions. Occurrences accept exact logical_paths and occurrence_ids;
nodes accept artifact_ids. Batch filters intersect (omitted means all available, []
means none); fields selects columns while retaining identity links. source_complete
describes retained graph membership; source_coverage describes producer coverage,
including unknown encrypted PCK directories. Page exhaustion does not prove absence in a
partial source.
## inspect_analysis_view: WebAssembly
WASM views use kind wasm and facet
sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets
use UTF-16 code units and source forms remain exact.
## inspect_analysis_view: Mach-O
Mach-O views use kind macho and facet
entrypoints/architectures/build_metadata/load_commands/dependencies/imports/exports/segments/provenance;
source_exhaustive false means unreturned rows may exist.
## inspect_analysis_view: Interface Builder
Interface Builder views use kind interface-builder and collection
documents/nodes/edges/coverage; nodes and edges accept an exact document relative_path.
## inspect_analysis_view: EVM interfaces
EVM summary returns all inferred interface candidates, carrier/decoded identities and
explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet
functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate
rows, and text offsets/limits count UTF-16 code units (hex has two characters per
decoded byte). Exhausting candidate pages does not establish global interface
completeness.
## inspect_analysis_view: Managed artifacts and members
Managed artifact summaries preserve complete identity/classification, metadata status,
module/MVID, frameworks, coverage and limitations without unrelated inventories. Managed
pages use kind managed and collection references/resources/attributes, retaining source
coverage and build-local token scope; page exhaustion does not establish complete
metadata. Managed member summaries keep identity, selection with unmatched names,
coverage and per-collection counts; managed-members pages use collection
types/exported_types/fields/methods/member_refs/call_edges/field_accesses/user_strings.
Exact names and declaring_types filters intersect before paging where supported; fields
selects columns (distinct from collection: "fields"). Identity/source_index, module
MVID, source coverage and unresolved filter counts remain; omit body to avoid IL
anchors. Omitted fields preserves full rows.
## inspect_analysis_view: Android manifests
Android package summaries retain all declarations, engine identity and counts without
decoded manifest text. Android manifest pages use kind android, facet manifest, offset
and limit in UTF-16 code units; source_status preserves partial decoded coverage even
when the retained text page is exhausted.
## inspect_analysis_view: Dart AOT
Dart AOT summary preserves target/ABI/library identity, snapshot headers and pool lower
bounds. String pages use kind dart-aot, collection
package_uris/dart_uris/class_like_tokens; source coverage remains partial when the
producer says partial.
## inspect_analysis_view: Protobuf
Protobuf views use kind protobuf, collection bodies/frames/fields, with record_ordinals,
body_pointers and fields; field pages also select frame_indices, field_ids,
field_numbers or schema_names. Byte identity, offset basis and body status remain
present. Text and schema_value are opt-in page columns.
## inspect_analysis_view: APK signatures
APK signing views use kind apk-signature and collection signers/digests/lineage, with
ids, signer_ids, schemes, fields and paging. Identity links remain when fields is empty.
Declared identities and digest matches do not establish signature verification or
authenticity.
## inspect_analysis_view: Model artifacts
Model views use kind model-artifact and collection metadata (keys), tensors
(names/types) or types (derived totals), with indices, fields and paging. Header SHA-256
scopes declarations; full-file identity remains unknown unless full hashing was
selected. Arrays retain typed eight-element previews with explicit omissions.
## inspect_analysis_view: IL2CPP metadata
IL2CPP metadata views use kind il2cpp-metadata and collection
sections/images/types/methods/fields/properties/events/parameters/string_literals/code_gen_modules/method_addresses/return_types/field_types/parameter_types/generic_method_instances/generic_method_issues.
Select indices, fields and pages;
sections/images/types/methods/fields/properties/events/parameters/return_types/field_types/parameter_types/generic_method_instances
accept exact names (native type names match retained rendered values, preserving
same-name rows; null names never match), types also accept
image_indices/namespaces/declaring_types, and methods/members accept declaring_types.
Parameters and parameter_types accept method_indices; code_gen_modules accept
image_indices; method_addresses and generic_method_instances accept addresses/states
(generic_method_instances also method_indices), preserving every folded alternative;
generic_method_issues accept rows. Row index is scoped to the metadata digest; native
pages add binary_source. Uncollected native tables have null counts and remain
unavailable.
## inspect_analysis_view: Desktop directories
Desktop directory views use kind desktop-application and collection
files/omissions/candidates/components/next_probes, with fields and pagination; files and
omissions accept exact paths or their stable IDs, and candidates accept families. Source
coverage and stable row IDs remain available.
## inspect_analysis_view: Native load images
Native load-image Evidence uses kind native-load-image with collection
source_files/mappings/relocations/entry_points/entry_context/checks/sections, original
indices, collection-specific fields and offset/limit paging. Null collections remain
unrecorded or inapplicable; pages preserve unsupported verification.
## inspect_analysis_view: Native inventories
Native inventory Evidence from list_names, list/search_procedures or list/search_strings
uses view: {kind: "native-inventory", offset: 0, limit: 64, fields: ["value"]}. Rows
keep source_index and address even with selected fields. Summary counts describe
retained rows; provider discovery coverage remains unknown.
## inspect_analysis_view: Native functions
Native function Evidence accepts kind summary for identity, availability and facet
counts, or kind native for selected facts: view: {kind: "native", facet: "procedure"}.
Facets select procedure, pseudocode, assembly, basic blocks, comments, callers/callees,
references, unresolved calls, referenced strings/names, native_api and value-flow
observations. For parameter type paths when value flow is recorded, use facet:
"value_flow_parameters"; its decompiler-observed path may be passed to
inspect_native_data_type, but exact database lookup can be unavailable and does not by
itself establish type identity. Use nonnegative offset and a positive caller-selected
limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use
rows.
## inspect_analysis_view: Batch decompilation
Retained batch_decompile Evidence accepts kind summary for counts and sizes, or view:
{kind: "batch-decompile", collection: "items", where: {statuses: ["unavailable",
"error"]}} for body-free rows with exact indices, addresses, procedure_addresses or
statuses (include_pseudocode adds bodies), or collection "pseudocode" with an index to
page one body.
## inspect_analysis_view: Evidence and transport
Returns artifact identity, parent Evidence ID, a distinct view digest, coverage,
limitations and unknowns. Actual serialized size determines MCP transport admission;
choose a smaller page or export the complete retained Evidence if needed.
`;
+1 -1
View File
@@ -17,7 +17,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
'Use native-procedure-references to page retained procedure reference or unresolved-call rows. Endpoint kind selected-procedure refers to the summary procedure, null preserves the original null, and shared carries an index into items.endpoints, where each other procedure is stored once per page. Exact source_address and target_address filters apply to reference rows. For inspect_api_contract parents, graphql-contract pages retained SDL types/fields, operation definitions, field references, fragment-spreads or diagnostics with exact filters and selected columns. Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Use web-bundle collections endpoints or inferences to page endpoint observations and inferences from retained analyze_web_bundle Evidence; summary returns target URL, origin, and counts. For retained trace_feature Evidence, kind native-feature-trace pages matches, references or unknowns; matches filter exact address, references filter exact source_address and target_address, source_index identifies each original row, and list_unknowns pages durable question heads. Inspect selected views of supported completed Evidence without repeating analysis. Use recorded-array with an exact raw_result object-member path to page a provider-recorded array when the producer supplies that path; rows keep their raw meaning and the parent retains coverage. For procedure_pseudo_code Evidence, summary reports text availability and the original selection; kind native with facet pseudocode pages the original UTF-16 text. Other native facets require analyze_function. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact names/indices/fields and matching-row pages; import modules intersects function names. source_index identifies original parent-and-collection rows, preserving every arity and raw atom index. beam.counts preserves null missing tables versus decoded empty tables; runtime absence remains unknown. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool hLine truncated
'Inspect selected views of completed Evidence without rerunning analysis or reopening its source. Use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}; summary receipts name the complete parent in normalized_result.parent_evidence_id. Portable sources use {kind: "inline", evidence: <complete Evidence>} or {kind: "evidence-file", path: "<absolute path>", evidence_id?: "<id in bundle>"}. Source kind is required. Choose a view compatible with the parent operation. For format-specific kinds, selectors and examples, call search_guidance with query "inspect_analysis_view <format or view kind>", then read_guidance_section with the returned locator; hot/cold clients discover those named tools first. Pagination preserves parent identity, source coverage, limitations and unknowns; exhausting a page does not establish complete source discovery. Text offsets use UTF-16 code units where documented. Reduce the selected page or export complete retained Evidence if the reply exceeds the transport budget.',
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [
+1 -1
View File
@@ -100,7 +100,7 @@ export const APPLICATION_TOOL_CONTRACTS = [
name: "search_guidance",
...toolContractMetadata("search_guidance"),
description:
"Search only the installed REA workflow guidance. Every non-generic query word must occur in a section; sections holding every word in their own text rank before parents whose words are spread across subsections. Returns ranked sections, bounded pages, and locators for read_guidance_section. No target or provider is accessed.",
"Search the installed REA workflow and tool guidance. Every non-generic query word must occur in a section; sections holding every word in their own text rank before parents whose words are spread across subsections. Returns ranked sections, bounded pages, and locators for read_guidance_section. No target or provider is accessed.",
kind: "application",
inputSchema: searchGuidanceInputSchema,
outputSchema: searchGuidanceOutputSchema,
+1
View File
@@ -15,6 +15,7 @@ export interface RecordedMcpCatalog {
readonly capabilities: ServerCapabilities;
readonly instructions: string;
readonly identity: ReturnType<typeof createCatalogIdentity>;
/** Output projections precede the SDK's negotiated root-result codec. */
readonly tools: readonly Tool[];
/** Tool names derived from canonical Evidence-input declarations at build time. */
readonly evidenceReferenceAlternatives: readonly string[];
+62 -4
View File
@@ -81,16 +81,28 @@ const fixture = async (
readonly maximumInputElements?: number;
readonly evidenceReferenceAlternative?: boolean;
readonly progressive?: boolean;
readonly unionOutput?: boolean;
readonly mode?: "legacy" | "auto";
} = {},
) => {
const started = barrier();
const calls = vi.fn<(n: number, context: ServerContext) => void>();
const closed = vi.fn();
let loads = 0;
const canonicalOutput = options.unionOutput
? z.union([output, z.strictObject({ message: z.string() })])
: output;
const recorded = {
...catalog,
tools: catalog.tools.map((tool) => ({
...tool,
outputSchema: z.toJSONSchema(canonicalOutput),
})),
};
const server = new DeferredEvidenceMcpServer(
options.evidenceReferenceAlternative
? { ...catalog, evidenceReferenceAlternatives: ["fixture"] }
: catalog,
? { ...recorded, evidenceReferenceAlternatives: ["fixture"] }
: recorded,
new ToolResultDelivery(1024 * 1024),
async (target) => {
loads++;
@@ -106,7 +118,7 @@ const fixture = async (
target.materialize(() => {
target.registerTool(
"fixture",
{ inputSchema: input, outputSchema: output },
{ inputSchema: input, outputSchema: canonicalOutput },
async (args, context) => {
calls(args.n, context);
await options.execute?.();
@@ -146,7 +158,12 @@ const fixture = async (
},
options.maximumInputElements,
);
const client = new Client({ name: "deferred-fixture-client", version: "1" });
const client = new Client(
{ name: "deferred-fixture-client", version: "1" },
options.mode === undefined
? undefined
: { versionNegotiation: { mode: options.mode } },
);
if (options.progressive) server.enableProgressiveCatalog();
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
@@ -300,6 +317,47 @@ it("preserves output validation after deferred registration", async () => {
expect(result.isError).toBe(true);
});
it.each(["legacy", "auto"] as const)(
"canonically validates deferred object-union outputs over %s",
async (mode) => {
const valid = await fixture({ unionOutput: true, mode });
const before = await valid.client.listTools();
const schema = before.tools.find(
({ name }) => name === "fixture",
)?.outputSchema;
if (schema === undefined) throw new Error("Missing union output schema");
expect(schema).toHaveProperty("anyOf");
expect(schema).not.toHaveProperty("properties.result");
if (mode === "legacy") expect(schema.type).toBe("object");
expect(valid.loads()).toBe(0);
const success = await valid.client.callTool({
name: "fixture",
arguments: { n: 7 },
});
expect(success.isError).not.toBe(true);
expect(success.structuredContent).toEqual({ n: 7 });
expect(valid.calls).toHaveBeenCalledOnce();
expect(await valid.client.listTools()).toEqual(before);
const invalid = await fixture({
unionOutput: true,
invalidOutput: true,
mode,
});
const rejected = await invalid.client.callTool({
name: "fixture",
arguments: {},
});
// The handler actually returned malformed data. A server-side validation
// error must arrive, rather than success or only a client-validator throw.
expect(invalid.calls).toHaveBeenCalledOnce();
expect(rejected.isError).toBe(true);
expect(rejected.structuredContent).toBeUndefined();
expect(invalid.loads()).toBe(1);
expect(await invalid.client.listTools()).toEqual(before);
},
);
it("initializes through prompt retrieval or completion without an internal client", async () => {
const f = await fixture();
expect(
+19 -1
View File
@@ -10,7 +10,7 @@ afterEach(async () => {
});
const connect = async (
options: { replyBytes?: number; bulk?: boolean } = {},
options: { replyBytes?: number; bulk?: boolean; guidance?: boolean } = {},
) => {
const bulkNames = options.bulk
? Array.from(
@@ -44,6 +44,7 @@ const connect = async (
"alpha_sqlite",
"beta_sqlite",
"excluded_sqlite",
...(options.guidance ? ["inspect_analysis_view"] : []),
...bulkNames,
]) {
const registration = server.registerTool(
@@ -188,6 +189,23 @@ it("loads exact names atomically and emits one notification for a batch", async
expect(f.changed).toHaveBeenCalledTimes(1);
});
it("finds deferred tool guidance keywords without advertising that guidance", async () => {
const f = await connect({ guidance: true });
const found = await f.discover({ query: "generic_method_issues" });
expect(found.isError).not.toBe(true);
expect(found.structuredContent).toMatchObject({
total_matches: 1,
tools: [{ name: "inspect_analysis_view" }],
});
const tool = (await f.client.listTools()).tools.find(
({ name }) => name === "inspect_analysis_view",
);
expect(tool?.description).toBe(
"inspect_analysis_view SQLite database inspection",
);
expect(f.changed).toHaveBeenCalledTimes(1);
});
it.each([
{ names: [] },
{ names: ["alpha_sqlite"], query: "sqlite" },
+2 -1
View File
@@ -7,6 +7,7 @@ import {
import { ownedJsonObjectSchema } from "../domain/jsonValue.js";
import { encodeToolResult } from "./toolResultEncoding.js";
import type { ToolResultDelivery } from "./ToolResultDelivery.js";
import { TOOL_GUIDANCE } from "../toolGuidance.js";
import {
DISCOVER_TOOLS,
HOT_TOOL_NAMES,
@@ -40,7 +41,7 @@ export class ProgressiveToolCatalog {
registered,
description: (registered.description ?? "").slice(0, 240),
search:
`${name} ${name.replaceAll("_", " ")} ${registered.description ?? ""}`.toLowerCase(),
`${name} ${name.replaceAll("_", " ")} ${registered.description ?? ""} ${TOOL_GUIDANCE[name]?.content ?? ""}`.toLowerCase(),
},
]),
);
+11
View File
@@ -0,0 +1,11 @@
import { ANALYSIS_VIEW_GUIDANCE } from "./contracts/analysisViewGuidance.js";
/** Compiled documentation shares guidance retrieval and catalog search ownership. */
export const TOOL_GUIDANCE: Readonly<
Record<string, { readonly guide: string; readonly content: string }>
> = {
inspect_analysis_view: {
guide: "tools/inspect_analysis_view.md",
content: ANALYSIS_VIEW_GUIDANCE,
},
};
@@ -9,8 +9,14 @@ import { apiContractSchema } from "../../../src/domain/apiContract/apiContract.j
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { toolOutputSchemaWithMetadata } from "../../../src/contracts/toolSchemaMetadata.js";
import { TOOL_GUIDANCE } from "../../../src/toolGuidance.js";
import { parseGuidanceSections } from "../../../src/application/GuidanceSections.js";
import {
searchGuidanceOutputSchema,
readGuidanceSectionOutputSchema,
} from "../../../src/contracts/guidanceContracts.js";
it("loads retained analysis views only when needed, preserving complete schemas, source facts and Evidence", async () => {
const verifyGuidanceAndRetainedView = async (profile: "full" | "hot-cold") => {
const root = await createTestTempDirectory("rea-progressive-api-");
const path = join(root, "api.json");
await writeFile(
@@ -22,12 +28,14 @@ it("loads retained analysis views only when needed, preserving complete schemas,
}),
);
const { client } = await connectLocalToolsMcp({
REA_MCP_TOOL_CATALOG_PROFILE: "hot-cold",
REA_MCP_TOOL_CATALOG_PROFILE: profile,
REA_MCP_TOOL_FAMILIES: "application,session",
});
const names = async () =>
(await client.listTools()).tools.map((tool) => tool.name);
expect(await names()).not.toContain("inspect_analysis_view");
expect((await names()).includes("inspect_analysis_view")).toBe(
profile === "full",
);
const changed = vi.fn();
client.setNotificationHandler("notifications/tools/list_changed", changed);
const call = async (name: string, args: Record<string, unknown>) => {
@@ -35,7 +43,10 @@ it("loads retained analysis views only when needed, preserving complete schemas,
expect(result.isError, JSON.stringify(result.content)).not.toBe(true);
return result;
};
await call("discover_tools", { names: ["inspect_api_contract"] });
const load = async (names: string[]) => {
if (profile === "hot-cold") await call("discover_tools", { names });
};
await load(["inspect_api_contract"]);
expect(await names()).toContain("inspect_api_contract");
const view = {
kind: "api-contract",
@@ -51,12 +62,53 @@ it("loads retained analysis views only when needed, preserving complete schemas,
kind: "api-contract",
items: [{ method: "GET", path: "/items", state: "declared" }],
});
expect(await names()).not.toContain("inspect_analysis_view");
expect(changed).toHaveBeenCalledTimes(1);
expect((await names()).includes("inspect_analysis_view")).toBe(
profile === "full",
);
expect(changed).toHaveBeenCalledTimes(profile === "hot-cold" ? 1 : 0);
await rm(path);
await call("discover_tools", {
names: ["inspect_analysis_view", "export_evidence_bundle"],
await load(["search_guidance", "read_guidance_section"]);
const found = await call("search_guidance", {
query: "inspect_analysis_view OpenAPI",
});
// Legacy MCP wraps root unions; auto-negotiated results use the object directly.
const unwrap = (value: unknown) =>
typeof value === "object" && value !== null && "result" in value
? value.result
: value;
const matches = searchGuidanceOutputSchema.parse(
unwrap(found.structuredContent),
);
if (matches.status !== "matches") throw new Error("Expected tool guidance");
const selectedGuide = matches.matches[0];
const guidance = TOOL_GUIDANCE.inspect_analysis_view!;
expect(selectedGuide?.guide).toBe(guidance.guide);
const read = await call("read_guidance_section", {
locator: selectedGuide?.locator,
});
const section = readGuidanceSectionOutputSchema.parse(
unwrap(read.structuredContent),
);
if (section.status !== "section") throw new Error("Expected a guide section");
const owner = parseGuidanceSections(
new Map([[guidance.guide, guidance.content]]),
)?.find(({ heading }) => heading === selectedGuide?.heading);
expect(owner).toBeDefined();
expect(section.content).toBe(owner?.content);
if (profile === "hot-cold") {
const miss = await call("discover_tools", {
query: "inspect_analysis_view qzxv",
});
expect(miss.structuredContent).toMatchObject({ total_matches: 0 });
// A keyword formerly present only in the oversized description still finds the view.
const foundView = await call("discover_tools", {
query: "generic_method_issues",
});
expect(foundView.structuredContent).toMatchObject({
tools: [expect.objectContaining({ name: "inspect_analysis_view" })],
});
}
await load(["inspect_analysis_view", "export_evidence_bundle"]);
const loaded = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_analysis_view",
);
@@ -65,7 +117,10 @@ it("loads retained analysis views only when needed, preserving complete schemas,
"~standard"
].jsonSchema.output({ target: "draft-2020-12" }),
);
expect(changed).toHaveBeenCalledTimes(2);
expect(loaded?.description).toBe(
toolContract("inspect_analysis_view").description,
);
expect(changed).toHaveBeenCalledTimes(profile === "hot-cold" ? 4 : 0);
const source = {
kind: "retained-evidence",
evidence_id: selected.parent_evidence_id,
@@ -93,6 +148,11 @@ it("loads retained analysis views only when needed, preserving complete schemas,
expect(follow.structuredContent).toHaveProperty("evidence_links", [
selected.parent_evidence_id,
]);
await call("discover_tools", { names: ["inspect_analysis_view"] });
expect(changed).toHaveBeenCalledTimes(2);
});
await load(["inspect_analysis_view"]);
expect(changed).toHaveBeenCalledTimes(profile === "hot-cold" ? 4 : 0);
};
it.each(["full", "hot-cold"] as const)(
"retrieves view guidance and retained facts with the %s catalog, preserving complete schemas and Evidence",
verifyGuidanceAndRetainedView,
);
+42 -1
View File
@@ -9,7 +9,7 @@ it("finds and retrieves the selected section from the generated package bundle",
const resources = new ArtifactResourceScope();
try {
const result = await searchGuidance(
{ query: "exact JavaScript module" },
{ query: "exact JavaScript module without repeating analysis" },
resources,
);
if (!result.ok) throw result.error;
@@ -52,6 +52,47 @@ it("finds and retrieves the selected section from the generated package bundle",
}
});
it("keeps workflow and selector guidance reachable for a broad module query", async () => {
const resources = new ArtifactResourceScope();
try {
const result = await searchGuidance(
{ query: "exact JavaScript module" },
resources,
);
if (!result.ok) throw result.error;
if (result.value.status !== "matches")
throw new Error("Expected guidance matches");
const workflow = result.value.matches.find(
({ guide, heading }) =>
guide === "references/javascript-applications.md" &&
heading === "Read a module without repeating analysis",
);
expect(workflow).toBeDefined();
const selectors = result.value.matches.find(
({ guide, heading }) =>
guide === "tools/inspect_analysis_view.md" &&
heading === "inspect_analysis_view: JavaScript modules and facts",
);
expect(selectors).toBeDefined();
if (selectors === undefined) throw new Error("Expected selector guidance");
const read = await readGuidanceSection(
{ locator: selectors.locator },
resources,
);
if (!read.ok) throw read.error;
expect(read.value).toMatchObject({
status: "section",
guide: selectors.guide,
heading: selectors.heading,
content: expect.stringContaining(
"read module without repeating analysis",
),
});
} finally {
await resources.close();
}
});
it("finds focused installed guidance for switching providers with annotations", async () => {
const resources = new ArtifactResourceScope();
try {
+75 -3
View File
@@ -24,6 +24,10 @@ import { HOT_TOOL_NAMES } from "../../../src/catalogDiscovery.js";
import { DatabaseSync } from "node:sqlite";
import { Ajv2020 } from "ajv/dist/2020.js";
import type { AnySchemaObject } from "ajv";
import {
searchGuidanceOutputSchema,
readGuidanceSectionOutputSchema,
} from "../../../src/contracts/guidanceContracts.js";
const connect = async (
mode: "legacy" | "auto",
@@ -277,7 +281,35 @@ it.each([
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
});
const before = await client.listTools();
expect(before.tools).toEqual(recorded.tools);
// Compare independently composed canonical SDK registrations as well as
// recorded artifacts. Listing every explicit family selects eager startup
// with the same inventory, without target analysis or provider acquisition.
const eager = await connect(mode, {
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
REA_MCP_TOOL_FAMILIES: TOOL_FAMILY_DEFINITIONS.map(({ id }) => id).join(
",",
),
});
expect(before).toEqual(await eager.client.listTools());
const withoutOutput = ({
outputSchema: _output,
...tool
}: {
outputSchema?: unknown;
}) => tool;
expect(before.tools.map(withoutOutput)).toEqual(
recorded.tools.map(withoutOutput),
);
const outputs = new Map(
(mode === "auto" ? recorded.tools : GENERATED_MCP_TOOL_CATALOG).map(
(tool: { name: string; outputSchema?: unknown }) => [
tool.name,
tool.outputSchema,
],
),
);
for (const tool of before.tools)
expect(tool.outputSchema, tool.name).toEqual(outputs.get(tool.name));
const contractProjection = (tool: {
name: string;
title?: string | undefined;
@@ -293,7 +325,7 @@ it.each([
outputSchema: tool.outputSchema,
annotations: tool.annotations,
});
if (profile === "full")
if (profile === "full" && mode === "legacy")
expect(
new Map(
before.tools.map((tool) => [tool.name, contractProjection(tool)]),
@@ -358,6 +390,38 @@ it.each([
},
);
it.each(["legacy", "auto"] as const)(
"validates non-object-root guidance results from deferred startup over %s",
async (mode) => {
const { client, stderr } = await connect(mode);
const before = await client.listTools();
const unwrap = (value: unknown) =>
typeof value === "object" && value !== null && "result" in value
? value.result
: value;
const found = await client.callTool({
name: "search_guidance",
arguments: { query: "exact JavaScript module", limit: 1 },
});
expect(found.isError).not.toBe(true);
const matches = searchGuidanceOutputSchema.parse(
unwrap(found.structuredContent),
);
if (matches.status !== "matches") throw new Error("Expected guide matches");
const read = await client.callTool({
name: "read_guidance_section",
arguments: { locator: matches.matches[0]?.locator },
});
expect(read.isError).not.toBe(true);
expect(
readGuidanceSectionOutputSchema.parse(unwrap(read.structuredContent))
.status,
).toBe("section");
expect(await client.listTools()).toEqual(before);
expect(stderr()).toBe("");
},
);
it("composes a scoped registration instead of serving the recorded catalog", async () => {
const { client, stderr } = await connect("legacy", {
REA_MCP_TOOL_FAMILIES: "session",
@@ -729,7 +793,12 @@ it.each([{}, { REA_MCP_OUTPUT_SCHEMA_PROFILE: "off" }])(
({ outputSchema: _output, ...tool }: Record<string, unknown>) =>
tool,
)
: catalog.tools;
: catalog.tools.map((tool: { name: string }) => ({
...tool,
outputSchema: GENERATED_MCP_TOOL_CATALOG.find(
({ name }) => name === tool.name,
)?.outputSchema,
}));
expect(before.tools).toEqual(expected);
const toolDefinition = before.tools.find(
({ name }) => name === "list_unknowns",
@@ -806,6 +875,7 @@ const RECORDER_RUNTIME_MODULES = [
"contracts/promptContracts.js",
"contracts/toolContracts.js",
"domain/evidenceInputContract.js",
"contracts/toolSchemaMetadata.js",
] as const;
// Recorder cleanup tests replace every runtime import through their loader
@@ -918,6 +988,8 @@ it.each([
"/dist/contracts/toolContracts.js": "export const TOOL_CONTRACTS = [];",
"/dist/domain/evidenceInputContract.js":
"export const getEvidenceInputContract = () => ({ hasEvidenceReferenceAlternative: false });",
"/dist/contracts/toolSchemaMetadata.js":
"export const toolOutputSchemaWithMetadata = () => { throw new Error('No fixture tools'); };",
};
const clientModule = `data:text/javascript,${encodeURIComponent(`
export class Client {
@@ -0,0 +1,793 @@
import {
Client,
SdkError,
SdkErrorCode,
type Tool,
} from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { cp, readFile, rm, symlink, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { afterAll, expect, it, onTestFinished, vi } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { closeVerifierResources } from "../../../scripts/lib/verifier-cleanup.mjs";
import { HOT_TOOL_NAMES } from "../../../src/catalogDiscovery.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import {
readGuidanceSectionOutputSchema,
searchGuidanceOutputSchema,
} from "../../../src/contracts/guidanceContracts.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js";
import { apiContractSchema } from "../../../src/domain/apiContract/apiContract.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { SERVER_START_FAILED } from "../../../src/main/messages.js";
const validation = createMcpSchemaValidation();
afterAll(() => validation.clear());
type Configuration = {
mode: "legacy" | "auto";
input: "full" | "compact";
output: "full" | "compact" | "off";
catalog: "full" | "hot-cold";
};
// Pairwise inputs across the protocol/output/catalog product: twelve independent
// clients, not an exhaustive 24-cell product. Compact/off output intentionally
// use eager composition; full output must remain deferred until a domain call.
const matrix: Configuration[] = ["legacy", "auto"].flatMap((mode, m) =>
["full", "compact", "off"].flatMap((output, o) =>
["full", "hot-cold"].map((catalog, c) => ({
mode: mode as Configuration["mode"],
output: output as Configuration["output"],
catalog: catalog as Configuration["catalog"],
input: (m + o + c) % 2 === 0 ? "full" : "compact",
})),
),
);
const lifecycleConfigurations = [
{ mode: "legacy", input: "full", output: "full", catalog: "full" },
{ mode: "auto", input: "compact", output: "full", catalog: "hot-cold" },
] as const;
const connect = async (
configuration: Configuration,
nodeArguments: readonly string[] = [],
entrypoint = resolve("scripts/rea.mjs"),
) => {
const root = await createTestTempDirectory("rea-deferred-schema-profiles-");
const log = join(root, "runtime-loaded");
await writeFile(log, "");
const hook = `data:text/javascript,${encodeURIComponent(`
import { registerHooks } from "node:module";
import { appendFileSync } from "node:fs";
registerHooks({
load(url, context, next) {
const loaded = next(url, context);
if (url.endsWith("/dist/main/eagerRuntime.js") ||
loaded.source?.toString().includes("//#region dist/main/eagerRuntime.js\\n"))
appendFileSync(${JSON.stringify(log)}, "loaded\\n");
return loaded;
}
});
`)}`;
const transport = new StdioClientTransport({
command: process.execPath,
args: [`--import=${hook}`, ...nodeArguments, entrypoint, "mcp"],
env: {
PATH: process.env.PATH ?? "",
HOME: root,
REA_LOG_LEVEL: "silent",
REA_MCP_INPUT_SCHEMA_PROFILE: configuration.input,
REA_MCP_OUTPUT_SCHEMA_PROFILE: configuration.output,
REA_MCP_TOOL_CATALOG_PROFILE: configuration.catalog,
},
stderr: "pipe",
});
let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr += chunk;
});
const client = new Client(
{ name: "deferred-schema-profile-client", version: "1" },
{
versionNegotiation: { mode: configuration.mode },
jsonSchemaValidator: validation,
},
);
let subscription: Awaited<ReturnType<Client["listen"]>> | undefined;
const close = () =>
closeVerifierResources([
() => subscription?.close() ?? Promise.resolve(),
() => client.close(),
() => transport.close(),
]);
onTestFinished(close);
await client.connect(transport);
let definitions = new Map<string, Tool>();
const list = async () => {
const result = await client.listTools();
definitions = new Map(result.tools.map((tool) => [tool.name, tool]));
return result;
};
const definition = (name: string) => {
const found = definitions.get(name);
if (found === undefined) throw new Error(`Tool not advertised: ${name}`);
return found;
};
const call = (
name: string,
arguments_: Record<string, unknown>,
signal?: AbortSignal,
) =>
client.callTool(
{ name, arguments: arguments_ },
{
toolDefinition: definition(name),
...(signal === undefined ? {} : { signal }),
},
);
const changed = vi.fn();
client.setNotificationHandler("notifications/tools/list_changed", changed);
if (configuration.mode === "auto")
subscription = await client.listen({ toolsListChanged: true });
const load = async (names: string[]) => {
if (configuration.catalog === "hot-cold") {
const result = await call("discover_tools", { names });
expect(result.isError).not.toBe(true);
await list();
expect(result.structuredContent).toMatchObject({
tools: [...new Set(names)].map((name) => ({
name,
input_schema: definition(name).inputSchema,
})),
});
}
};
return {
root,
client,
list,
definition,
call,
load,
changed,
close,
runtimeLoaded: async () => (await readFile(log, "utf8")).length > 0,
stderr: () => stderr,
};
};
const unwrapGuidance = (value: unknown) =>
typeof value === "object" && value !== null && "result" in value
? value.result
: value;
const verifyGuidance = async (
f: Awaited<ReturnType<typeof connect>>,
configuration: Configuration,
) => {
// The first domain call uses a root union, before any incidental materialization.
const found = await f.call("search_guidance", {
query: "exact JavaScript module",
limit: 1,
});
expect(found.isError).not.toBe(true);
expect(found.structuredContent).toHaveProperty(
configuration.mode === "legacy" ? "result.status" : "status",
"matches",
);
expect(
searchGuidanceOutputSchema.parse(unwrapGuidance(found.structuredContent))
.status,
).toBe("matches");
expect(await f.runtimeLoaded()).toBe(true);
const missing = await f.call("search_guidance", {
query: "unmatched_qzxv",
});
expect(
searchGuidanceOutputSchema.parse(unwrapGuidance(missing.structuredContent))
.status,
).toBe("no_matches");
const badGuidance = await f.call("search_guidance", { query: 17 });
expect(parseMcpToolError(badGuidance).error.code).toBe("invalid_request");
expect(badGuidance.structuredContent).toBeUndefined();
const guide = await f.call("search_guidance", {
query: "inspect_analysis_view OpenAPI",
limit: 1,
});
const matches = searchGuidanceOutputSchema.parse(
unwrapGuidance(guide.structuredContent),
);
if (matches.status !== "matches")
throw new Error("Expected compiled tool guidance");
expect(matches.matches[0]?.guide).toBe("tools/inspect_analysis_view.md");
const read = await f.call("read_guidance_section", {
locator: matches.matches[0]?.locator,
});
expect(read.isError).not.toBe(true);
expect(
readGuidanceSectionOutputSchema.parse(
unwrapGuidance(read.structuredContent),
).status,
).toBe("section");
};
it.each(matrix)(
"preserves schemas, guidance and retained Evidence over $mode with $input inputs, $output outputs and $catalog catalog",
async (configuration) => {
const f = await connect(configuration);
const initial = await f.list();
expect(f.client.getProtocolEra()).toBe(
configuration.mode === "legacy" ? "legacy" : "modern",
);
const eager = configuration.output !== "full";
expect(await f.runtimeLoaded()).toBe(eager);
if (configuration.catalog === "hot-cold") {
expect(new Set(initial.tools.map(({ name }) => name))).toEqual(
new Set([...HOT_TOOL_NAMES, "discover_tools"]),
);
} else {
expect(initial.tools.map(({ name }) => name)).toContain(
"inspect_analysis_view",
);
}
await f.load([
"search_guidance",
"read_guidance_section",
"inspect_api_contract",
"inspect_analysis_view",
]);
expect(await f.runtimeLoaded()).toBe(eager);
const before = await f.list();
for (const name of [
"search_guidance",
"read_guidance_section",
"inspect_api_contract",
"inspect_analysis_view",
]) {
const definition = f.definition(name);
expect(
validation.assertions.validateSchema(definition.inputSchema),
name,
).toBe(true);
if (configuration.output === "off")
expect(definition.outputSchema, name).toBeUndefined();
else {
if (definition.outputSchema === undefined)
throw new Error(`Missing output schema: ${name}`);
expect(
validation.assertions.validateSchema(definition.outputSchema),
name,
).toBe(true);
}
}
await verifyGuidance(f, configuration);
expect(await f.list()).toEqual(before);
const path = join(f.root, "api.json");
await writeFile(
path,
JSON.stringify({
openapi: "3.1.0",
info: { title: "Schema matrix", version: "1" },
paths: {
"/items": { get: { operationId: "listItems" } },
"/items/{id}": { delete: { operationId: "removeItem" } },
},
}),
);
const produced = await f.call("inspect_api_contract", {
path,
detail: "complete",
});
expect(produced.isError).not.toBe(true);
const parent = parseEvidence(produced.structuredContent);
expect(
apiContractSchema.parse(parent.normalized_result).operations,
).toMatchObject([
{
method: "GET",
path: "/items",
declared_id: "listItems",
source_pointer: "/paths/~1items/get",
},
{
method: "DELETE",
path: "/items/{id}",
declared_id: "removeItem",
source_pointer: "/paths/~1items~1{id}/delete",
},
]);
await rm(path);
const source = {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
};
const view = {
kind: "api-contract",
collection: "operations",
paths: ["/items"],
fields: ["method", "path"],
limit: 1,
};
const malformed = { source, view: { ...view, offset: -1 } };
expect(
toolContract("inspect_analysis_view").inputSchema.safeParse(malformed)
.success,
).toBe(false);
expect(
validation.assertions.validate(
f.definition("inspect_analysis_view").inputSchema,
{ source, view },
),
).toBe(true);
// Compact advertisements may safely retain more canonical constraints.
// Exact rejection belongs to the full projection and the server parser.
if (configuration.input === "full")
expect(
validation.assertions.validate(
f.definition("inspect_analysis_view").inputSchema,
malformed,
),
).toBe(false);
const rejected = await f.call("inspect_analysis_view", malformed);
expect(parseMcpToolError(rejected).error.code).toBe("invalid_request");
expect(rejected.structuredContent).toBeUndefined();
const selected = await f.call("inspect_analysis_view", { source, view });
expect(selected.isError).not.toBe(true);
const evidence = parseEvidence(selected.structuredContent);
expect(
analysisViewResultSchema.parse(evidence.normalized_result),
).toMatchObject({
parent_evidence_id: parent.evidence_id,
items: [{ method: "GET", path: "/items", state: "declared" }],
});
expect(evidence.evidence_links).toEqual([parent.evidence_id]);
const repeated = await f.call("inspect_analysis_view", { source, view });
expect(parseEvidence(repeated.structuredContent)).toEqual(evidence);
const unknown = await f.call("inspect_analysis_view", {
source: { ...source, evidence_id: `ev_${"b".repeat(64)}` },
view,
});
expect(parseMcpToolError(unknown).error.code).toBe(
"evidence_integrity_mismatch",
);
expect(unknown.structuredContent).toBeUndefined();
const notifications = f.changed.mock.calls.length;
await f.load([
"inspect_analysis_view",
"search_guidance",
"inspect_analysis_view",
]);
expect(f.changed).toHaveBeenCalledTimes(notifications);
expect(notifications).toBe(configuration.catalog === "hot-cold" ? 1 : 0);
expect(await f.list()).toEqual(before);
expect(f.stderr()).toBe("");
},
);
const verifyReconnect = async ({
configuration,
names,
parent,
view,
}: {
configuration: Configuration;
names: string[];
parent: ReturnType<typeof parseEvidence>;
view: Record<string, unknown>;
}) => {
const source = { kind: "retained-evidence", evidence_id: parent.evidence_id };
const next = await connect(configuration);
const fresh = await next.list();
expect(fresh.tools.map(({ name }) => name)).toEqual(
configuration.catalog === "hot-cold"
? names.filter(
(name) => HOT_TOOL_NAMES.has(name) || name === "discover_tools",
)
: names,
);
expect(await next.runtimeLoaded()).toBe(false);
await next.load(["inspect_analysis_view"]);
const expired = await next.call("inspect_analysis_view", {
source,
view,
});
expect(parseMcpToolError(expired).error.code).toBe(
"evidence_integrity_mismatch",
);
const portable = await next.call("inspect_analysis_view", {
source: { kind: "inline", evidence: parent },
view,
});
expect(portable.isError).not.toBe(true);
expect(
analysisViewResultSchema.parse(
parseEvidence(portable.structuredContent).normalized_result,
),
).toMatchObject({
parent_evidence_id: parent.evidence_id,
items: [{ method: "GET", path: "/peer" }],
});
expect(next.stderr()).toBe("");
};
it.each(lifecycleConfigurations)(
"keeps a peer callable after cancellation during $mode/$catalog materialization, then reconnects with fresh visibility and Evidence",
async (configuration) => {
const control = await createTestTempDirectory(
"rea-materialization-cancellation-",
);
const started = join(control, "started");
const release = join(control, "release");
// Hold the real lazy module at import time. Every request still passes
// through the production SDK transport, registration and canonical parser.
const gate = `await (async () => {
const fs = await import("node:fs/promises");
await fs.writeFile(${JSON.stringify(started)}, "started");
for (let attempt = 0; attempt < 1000; attempt++) {
try { await fs.access(${JSON.stringify(release)}); return; } catch {}
await new Promise(resolve => setTimeout(resolve, 5));
}
throw new Error("Fixture materialization was not released");
})();\n`;
const hook = `data:text/javascript,${encodeURIComponent(`
import { registerHooks } from "node:module";
registerHooks({
load(url, context, next) {
const loaded = next(url, context);
if (url.endsWith("/dist/main/eagerRuntime.js") ||
loaded.source?.toString().includes("//#region dist/main/eagerRuntime.js\\n")) {
return { ...loaded, source: ${JSON.stringify(gate)} + loaded.source.toString() };
}
return loaded;
}
});
`)}`;
const f = await connect(configuration, [`--import=${hook}`]);
await f.list();
await f.load([
"inspect_api_contract",
"inspect_analysis_view",
"export_evidence_bundle",
]);
expect(await f.runtimeLoaded()).toBe(false);
const before = await f.list();
const path = join(control, "peer.json");
const cancelledPath = join(control, "cancelled.json");
const api = (id: string) =>
JSON.stringify({
openapi: "3.1.0",
info: { title: "Cancellation fixture", version: "1" },
paths: { "/peer": { get: { operationId: id } } },
});
await writeFile(path, api("retainedPeer"));
await writeFile(cancelledPath, api("cancelledMustNotRun"));
const controller = new AbortController();
const reason = new SdkError(
SdkErrorCode.RequestTimeout,
"Cancelled during shared deferred initialization",
);
const cancelled = f.call(
"inspect_api_contract",
{ path: cancelledPath, detail: "complete" },
controller.signal,
);
const rejected = expect(cancelled).rejects.toBe(reason);
const pending: Promise<unknown>[] = [cancelled, rejected];
try {
const acknowledged = vi.waitFor(
async () => expect(await readFile(started, "utf8")).toBe("started"),
{ timeout: 2000, interval: 10 },
);
pending.push(acknowledged);
await awaitAcknowledgement(
acknowledged,
cancelled,
"public deferred materialization",
);
const peer = f.call("inspect_api_contract", { path, detail: "complete" });
pending.push(peer);
controller.abort(reason);
await rejected;
// A round trip while import is held also proves the catalog/protocol
// remain responsive after sending the cancellation notification.
expect(await f.list()).toEqual(before);
await writeFile(release, "release");
const result = await peer;
expect(result.isError).not.toBe(true);
const parent = parseEvidence(result.structuredContent);
expect(
apiContractSchema.parse(parent.normalized_result).operations,
).toMatchObject([
{ path: "/peer", method: "GET", declared_id: "retainedPeer" },
]);
expect(await f.runtimeLoaded()).toBe(true);
expect(await f.list()).toEqual(before);
const exportPath = join(control, "bundle.json");
expect(
(await f.call("export_evidence_bundle", { path: exportPath })).isError,
).not.toBe(true);
const bundle = evidenceBundleSchema.parse(
JSON.parse(await readFile(exportPath, "utf8")),
);
expect(
bundle.records
.filter(({ operation }) => operation === "inspect_api_contract")
.map(({ evidence_id }) => evidence_id),
).toEqual([parent.evidence_id]);
await rm(path);
const source = {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
};
const view = {
kind: "api-contract",
collection: "operations",
fields: ["method", "path"],
limit: 1,
};
const selected = await f.call("inspect_analysis_view", { source, view });
expect(selected.isError).not.toBe(true);
expect(
analysisViewResultSchema.parse(
parseEvidence(selected.structuredContent).normalized_result,
),
).toMatchObject({
parent_evidence_id: parent.evidence_id,
items: [{ method: "GET", path: "/peer" }],
});
await f.close();
await verifyReconnect({
configuration,
names: before.tools.map(({ name }) => name),
parent,
view,
});
expect(f.stderr()).toBe("");
} finally {
controller.abort(reason);
await writeFile(release, "release");
await Promise.allSettled(pending);
}
},
);
it.each(lifecycleConfigurations)(
"recovers from a malformed first domain call over $mode/$catalog without replacing advertised schemas",
async (configuration) => {
const f = await connect(configuration);
await f.list();
await f.load(["search_guidance", "read_guidance_section"]);
const before = await f.list();
expect(await f.runtimeLoaded()).toBe(false);
const malformed = await f.call("search_guidance", { query: 17 });
expect(parseMcpToolError(malformed).error.code).toBe("invalid_request");
expect(malformed.structuredContent).toBeUndefined();
expect(await f.runtimeLoaded()).toBe(true);
await verifyGuidance(f, configuration);
expect(await f.list()).toEqual(before);
expect(f.stderr()).toBe("");
},
);
it.each(lifecycleConfigurations)(
"keeps discovery responsive and rejects domain calls after $mode/$catalog materialization fails",
async (configuration) => {
// Fault the lazy module's evaluation, preserving its real imports/exports.
// Transport startup and discovery must still use the production server.
const hook = `data:text/javascript,${encodeURIComponent(`
import { registerHooks } from "node:module";
registerHooks({
load(url, context, next) {
const loaded = next(url, context);
if (url.endsWith("/dist/main/eagerRuntime.js") ||
loaded.source?.toString().includes("//#region dist/main/eagerRuntime.js\\n"))
return { ...loaded, source: 'throw new Error("PRIVATE fixture initialization failure");\\n' + loaded.source.toString() };
return loaded;
}
});
`)}`;
const f = await connect(configuration, [`--import=${hook}`]);
await f.list();
await f.load(["search_guidance", "read_guidance_section"]);
const before = await f.list();
expect(await f.runtimeLoaded()).toBe(false);
for (const query of ["inspect_analysis_view OpenAPI", "unmatched_qzxv"]) {
const result = await f.call("search_guidance", { query });
expect(result.isError).toBe(true);
expect(result.structuredContent).toBeUndefined();
expect(JSON.stringify(result)).toContain(SERVER_START_FAILED);
expect(JSON.stringify(result)).not.toContain("PRIVATE");
}
expect(await f.runtimeLoaded()).toBe(true);
await f.load(["search_guidance", "read_guidance_section"]);
expect(await f.list()).toEqual(before);
expect(f.stderr()).toBe("");
},
);
it.each(lifecycleConfigurations)(
"materializes through a prompt operation over $mode/$catalog and preserves schemas and live Evidence completion",
async (configuration) => {
const f = await connect(configuration);
await f.list();
await f.load([
"inspect_api_contract",
"export_evidence_bundle",
"search_guidance",
"read_guidance_section",
]);
const toolsBefore = await f.list();
const promptsBefore = await f.client.listPrompts();
expect(promptsBefore.prompts.map(({ name }) => name)).toContain(
"verify_reconstruction",
);
expect(await f.runtimeLoaded()).toBe(false);
const complete = () =>
f.client.complete({
ref: { type: "ref/prompt", name: "verify_reconstruction" },
argument: { name: "comparison_evidence_id", value: "ev_" },
});
// Use retrieval first on legacy, completion first on modern; neither
// operation should execute an analysis or create Evidence observations.
if (configuration.mode === "auto")
expect((await complete()).completion.values).toEqual([]);
const prompt = await f.client.getPrompt({
name: "investigate_feature",
arguments: { feature: "declared GET /items" },
});
expect(await f.runtimeLoaded()).toBe(true);
expect(prompt.messages).toHaveLength(1);
expect(prompt.messages[0]).toMatchObject({
role: "user",
content: {
type: "text",
text: expect.stringContaining('"feature":"declared GET /items"'),
},
});
if (configuration.mode === "legacy")
expect((await complete()).completion.values).toEqual([]);
await expect(
f.client.getPrompt({
name: "investigate_feature",
arguments: { feature: " " },
}),
).rejects.toThrow(/Invalid arguments/u);
expect(await f.client.listPrompts()).toEqual(promptsBefore);
expect(await f.list()).toEqual(toolsBefore);
const exportPath = join(f.root, "prompt-only.json");
expect(
(await f.call("export_evidence_bundle", { path: exportPath })).isError,
).not.toBe(true);
expect(
evidenceBundleSchema.parse(JSON.parse(await readFile(exportPath, "utf8")))
.records,
).toEqual([]);
const path = join(f.root, "completion.json");
await writeFile(
path,
JSON.stringify({
openapi: "3.1.0",
info: { title: "Prompt completion", version: "1" },
paths: { "/items": { get: { operationId: "listItems" } } },
}),
);
const result = await f.call("inspect_api_contract", {
path,
detail: "complete",
});
expect(result.isError).not.toBe(true);
const evidence = parseEvidence(result.structuredContent);
expect(
apiContractSchema.parse(evidence.normalized_result).operations,
).toMatchObject([{ method: "GET", path: "/items" }]);
expect((await complete()).completion.values).toEqual([
evidence.evidence_id,
]);
await verifyGuidance(f, configuration);
expect(await f.list()).toEqual(toolsBefore);
expect(f.stderr()).toBe("");
},
);
it.each(lifecycleConfigurations)(
"validates stale and unavailable guidance replies and recovers after bundle repair over $mode/$catalog",
async (configuration) => {
const runtime = await createTestTempDirectory(
"rea-guidance-failure-runtime-",
);
// Exercise the shipped dispatcher and runtime with an independently
// editable guide bundle. Only locked dependencies are linked to the build.
for (const path of ["dist", "skills", "scripts", "package.json"])
await cp(resolve(path), join(runtime, path), { recursive: true });
await symlink(
resolve("node_modules"),
join(runtime, "node_modules"),
"junction",
);
const guidePath = join(
runtime,
"skills/reverse-engineer-anything/SKILL.md",
);
const original = await readFile(guidePath);
const f = await connect(
configuration,
[],
join(runtime, "scripts/rea.mjs"),
);
await f.list();
await f.load(["search_guidance", "read_guidance_section"]);
const before = await f.list();
expect(await f.runtimeLoaded()).toBe(false);
const search = async (query: string, cursor?: string) => {
const result = await f.call("search_guidance", {
query,
limit: 1,
...(cursor === undefined ? {} : { cursor }),
});
expect(result.isError).not.toBe(true);
return searchGuidanceOutputSchema.parse(
unwrapGuidance(result.structuredContent),
);
};
const found = await search("inspect_analysis_view OpenAPI");
if (found.status !== "matches" || found.matches[0] === undefined)
throw new Error("Expected a compiled guidance locator");
expect(found.matches[0].guide).toBe("tools/inspect_analysis_view.md");
const locator = found.matches[0].locator;
const page = await search("inspect_analysis_view");
if (page.status !== "matches" || page.next_cursor === undefined)
throw new Error("Expected a guidance continuation token");
const read = async () => {
const result = await f.call("read_guidance_section", { locator });
expect(result.isError).not.toBe(true);
return readGuidanceSectionOutputSchema.parse(
unwrapGuidance(result.structuredContent),
);
};
await writeFile(
guidePath,
Buffer.concat([
original,
Buffer.from("\n<!-- bundle drift fixture -->\n"),
]),
);
expect((await read()).status).toBe("stale_locator");
expect(
(await search("inspect_analysis_view", page.next_cursor)).status,
).toBe("stale_continuation");
const invalid = await f.call("read_guidance_section", {
locator: "../../outside.md",
});
expect(parseMcpToolError(invalid).error.code).toBe("invalid_request");
expect(invalid.structuredContent).toBeUndefined();
await rm(guidePath);
expect((await search("inspect_analysis_view OpenAPI")).status).toBe(
"bundle_unavailable",
);
expect((await read()).status).toBe("bundle_unavailable");
await writeFile(guidePath, original);
expect(await search("inspect_analysis_view OpenAPI")).toMatchObject({
status: "matches",
bundle_sha256: found.bundle_sha256,
});
expect(await read()).toMatchObject({
status: "section",
guide: "tools/inspect_analysis_view.md",
locator,
content: expect.stringContaining("api-contract"),
});
expect(await f.runtimeLoaded()).toBe(true);
expect(await f.list()).toEqual(before);
expect(f.stderr()).toBe("");
},
);
+105 -52
View File
@@ -12,14 +12,25 @@ import {
} from "../../support/mcp/streamableHttpProcess.js";
import { processTest } from "../../support/process/processFixture.js";
import { assertAdvertisedMcpContracts } from "./mcpContractHarness.js";
import { searchGuidanceOutputSchema } from "../../../src/contracts/guidanceContracts.js";
const HTTP_BOUNDARY_TIMEOUT_MS = 90_000;
const openSession = async (url: URL, name: string) => {
const openSession = async (
url: URL,
name: string,
mode: "legacy" | "auto" = "legacy",
) => {
const transport = new StreamableHTTPClientTransport(url);
const client = new Client({ name, version: "1" });
const client = new Client(
{ name, version: "1" },
{ versionNegotiation: { mode } },
);
onTestFinished(() => client.close());
await client.connect(transport);
// HTTP deliberately composes the legacy server; auto must fall back before
// using its session lifecycle and unsolicited catalog-change notifications.
expect(client.getNegotiatedProtocolVersion()).toBe("2025-11-25");
expect(transport.sessionId).toEqual(expect.any(String));
const { toolDefinition } = await advertisedMcpToolSchemas(
client,
@@ -46,57 +57,99 @@ const expectIdentity = async ({
});
};
describe("rea mcp --http", () => {
processTest(
"keeps hot/cold visibility within its HTTP session and refreshes named contracts",
async ({ processes }) => {
const started = await startListeningProcess(
processes,
["mcp", "--http", "--port", "0"],
{ REA_MCP_TOOL_CATALOG_PROFILE: "hot-cold" },
);
const first = await openSession(started.url, "hot-cold-first");
const initialNames = (await first.client.listTools()).tools.map(
({ name }) => name,
);
expect(initialNames).not.toContain("inspect_analysis_view");
expect(initialNames).not.toContain("list_unknowns");
let changed = 0;
first.client.setNotificationHandler(
"notifications/tools/list_changed",
() => {
changed++;
},
);
const discovered = await first.client.callTool({
name: "discover_tools",
arguments: { names: ["list_unknowns"] },
});
expect(discovered.isError).not.toBe(true);
const { toolDefinition } = await advertisedMcpToolSchemas(
first.client,
"list_unknowns",
);
expect(changed).toBe(1);
expect(
(
await first.client.callTool(
{ name: "list_unknowns", arguments: {} },
{ toolDefinition },
)
).isError,
).not.toBe(true);
await first.transport.terminateSession();
await first.client.close();
const second = await openSession(started.url, "hot-cold-second");
expect(
(await second.client.listTools()).tools.map(({ name }) => name),
).toEqual(initialNames);
await expectIdentity(second);
await second.transport.terminateSession();
},
HTTP_BOUNDARY_TIMEOUT_MS,
/** Use the exact refreshed definition; root-union framing belongs to the SDK. */
const searchGuidance = async (client: Client, query: string) => {
const { toolDefinition } = await advertisedMcpToolSchemas(
client,
"search_guidance",
);
const result = await client.callTool(
{ name: "search_guidance", arguments: { query, limit: 1 } },
{ toolDefinition },
);
expect(result.isError).not.toBe(true);
const content = result.structuredContent;
return searchGuidanceOutputSchema.parse(
typeof content === "object" && content !== null && "result" in content
? content.result
: content,
);
};
describe("rea mcp --http", () => {
for (const mode of ["legacy", "auto"] as const)
processTest(
`${mode} keeps hot/cold visibility within its HTTP session and refreshes named contracts`,
async ({ processes }) => {
const started = await startListeningProcess(
processes,
["mcp", "--http", "--port", "0"],
{ REA_MCP_TOOL_CATALOG_PROFILE: "hot-cold" },
);
const first = await openSession(started.url, "hot-cold-first", mode);
const initialNames = (await first.client.listTools()).tools.map(
({ name }) => name,
);
expect(initialNames).not.toContain("inspect_analysis_view");
expect(initialNames).not.toContain("list_unknowns");
let changed = 0;
first.client.setNotificationHandler(
"notifications/tools/list_changed",
() => {
changed++;
},
);
const discovered = await first.client.callTool({
name: "discover_tools",
arguments: { names: ["list_unknowns", "search_guidance"] },
});
expect(discovered.isError).not.toBe(true);
const { toolDefinition } = await advertisedMcpToolSchemas(
first.client,
"list_unknowns",
);
expect(changed).toBe(1);
expect(
(
await first.client.callTool(
{ name: "list_unknowns", arguments: {} },
{ toolDefinition },
)
).isError,
).not.toBe(true);
expect(
await searchGuidance(first.client, "inspect_analysis_view OpenAPI"),
).toMatchObject({
status: "matches",
matches: [{ guide: "tools/inspect_analysis_view.md" }],
});
await first.client.callTool({
name: "discover_tools",
arguments: { names: ["search_guidance", "list_unknowns"] },
});
expect(changed).toBe(1);
await first.transport.terminateSession();
await first.client.close();
const second = await openSession(started.url, "hot-cold-second", mode);
expect(
(await second.client.listTools()).tools.map(({ name }) => name),
).toEqual(initialNames);
expect(
(
await second.client.callTool({
name: "discover_tools",
arguments: { names: ["search_guidance"] },
})
).isError,
).not.toBe(true);
expect(
(await searchGuidance(second.client, "unmatched_qzxv")).status,
).toBe("no_matches");
await expectIdentity(second);
await second.transport.terminateSession();
},
HTTP_BOUNDARY_TIMEOUT_MS,
);
processTest(
"serves initialize, tools/list, a tool call and DELETE, then keeps serving later clients",