mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 17:30:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9304152a95 | ||
|
|
f1622b32d5 |
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user