mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 13:56:47 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09980ead00 | ||
|
|
a7655ef29e | ||
|
|
531f2bbfb0 |
No files matched your search
@@ -29,6 +29,10 @@ are bound to the installed bundle and report stale or unavailable guidance witho
|
||||
returning cached section content. A no-match result says nothing about analysis
|
||||
capability availability.
|
||||
|
||||
Over MCP, both tools deliver their structured result under `result`, matching
|
||||
the object root of their advertised output schemas, as the session tools do.
|
||||
The CLI prints the result itself.
|
||||
|
||||
Search returns at most 20 matches per page with 500-code-unit context excerpts.
|
||||
Read retrieves the complete selected section. Admission permits 1 MiB per file,
|
||||
4 MiB per bundle, 4,096 sections, and 25,165,824 expanded UTF-16 code units before
|
||||
|
||||
@@ -29,9 +29,9 @@ import type { ToolContract } from "./toolContractTypes.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import {
|
||||
readGuidanceSectionInputSchema,
|
||||
readGuidanceSectionOutputSchema,
|
||||
readGuidanceSectionToolOutputSchema,
|
||||
searchGuidanceInputSchema,
|
||||
searchGuidanceOutputSchema,
|
||||
searchGuidanceToolOutputSchema,
|
||||
} from "./guidanceContracts.js";
|
||||
import {
|
||||
guidedWorkflowsInputSchema,
|
||||
@@ -86,7 +86,7 @@ export const APPLICATION_TOOL_CONTRACTS = [
|
||||
"Search only the installed REA workflow guidance. Every non-generic query word must occur in a section. Returns ranked sections, bounded pages, and locators for read_guidance_section. No target or provider is accessed.",
|
||||
kind: "application",
|
||||
inputSchema: searchGuidanceInputSchema,
|
||||
outputSchema: searchGuidanceOutputSchema,
|
||||
outputSchema: searchGuidanceToolOutputSchema,
|
||||
examples: [
|
||||
{
|
||||
title: "Find exact JavaScript module guidance",
|
||||
@@ -124,7 +124,7 @@ export const APPLICATION_TOOL_CONTRACTS = [
|
||||
"Retrieve a section selected by search_guidance from the installed REA guidance bundle. Reports stale locators and unavailable bundles explicitly. Returns documentation, not target observations or Evidence.",
|
||||
kind: "application",
|
||||
inputSchema: readGuidanceSectionInputSchema,
|
||||
outputSchema: readGuidanceSectionOutputSchema,
|
||||
outputSchema: readGuidanceSectionToolOutputSchema,
|
||||
examples: [],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -89,3 +89,17 @@ export type SearchGuidanceOutput = z.output<typeof searchGuidanceOutputSchema>;
|
||||
export type ReadGuidanceSectionOutput = z.output<
|
||||
typeof readGuidanceSectionOutputSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* MCP 2025 wire output roots are objects, so the adapters deliver each result
|
||||
* union under `result`, as session tools do. A root union would be wrapped in
|
||||
* the advertisement alone by the recorded catalog and rejected by validating
|
||||
* clients.
|
||||
*/
|
||||
export const searchGuidanceToolOutputSchema = z.strictObject({
|
||||
result: searchGuidanceOutputSchema,
|
||||
});
|
||||
|
||||
export const readGuidanceSectionToolOutputSchema = z.strictObject({
|
||||
result: readGuidanceSectionOutputSchema,
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { TOOL_CONTRACTS } from "./toolContracts.js";
|
||||
import { toolOutputSchemaWithMetadata } from "./toolSchemaMetadata.js";
|
||||
|
||||
// MCP 2025 output roots are objects. The stdio server registers recorded
|
||||
// advertisements, so a non-object root would be wrapped there alone and its
|
||||
// structured results would fail validating clients (#8304).
|
||||
it.each(["draft-2020-12", "draft-07"] as const)(
|
||||
"advertises an object output root for every tool in %s",
|
||||
(target) => {
|
||||
const nonObject = TOOL_CONTRACTS.filter(
|
||||
(contract) =>
|
||||
toolOutputSchemaWithMetadata(contract)["~standard"].jsonSchema.output({
|
||||
target,
|
||||
}).type !== "object",
|
||||
).map(({ name }) => name);
|
||||
expect(nonObject).toEqual([]);
|
||||
},
|
||||
);
|
||||
@@ -21,7 +21,7 @@ export const registerGuidanceTools = (
|
||||
const result = await searchGuidance(input, resources);
|
||||
return server.delivery.toCallToolResult(
|
||||
result.ok
|
||||
? { ok: true, value: jsonValueSchema.parse(result.value) }
|
||||
? { ok: true, value: jsonValueSchema.parse({ result: result.value }) }
|
||||
: result,
|
||||
search,
|
||||
);
|
||||
@@ -36,7 +36,7 @@ export const registerGuidanceTools = (
|
||||
const result = await readGuidanceSection(input, resources);
|
||||
return server.delivery.toCallToolResult(
|
||||
result.ok
|
||||
? { ok: true, value: jsonValueSchema.parse(result.value) }
|
||||
? { ok: true, value: jsonValueSchema.parse({ result: result.value }) }
|
||||
: result,
|
||||
read,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { resolve } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
readGuidanceSectionToolOutputSchema,
|
||||
searchGuidanceToolOutputSchema,
|
||||
} from "../../../src/contracts/guidanceContracts.js";
|
||||
|
||||
it(
|
||||
"delivers guidance results the validating SDK client accepts over the stdio server",
|
||||
{ timeout: 60_000 },
|
||||
async () => {
|
||||
const client = new Client({ name: "guidance-stdio-test", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [resolve("scripts/rea.mjs"), "mcp"],
|
||||
stderr: "pipe",
|
||||
});
|
||||
await client.connect(transport);
|
||||
try {
|
||||
// Loading the live definitions makes the SDK validate every result
|
||||
// against the advertised, SDK-converted output schema.
|
||||
const advertised = (await client.listTools()).tools.filter(({ name }) =>
|
||||
["search_guidance", "read_guidance_section"].includes(name),
|
||||
);
|
||||
expect(advertised.map(({ outputSchema }) => outputSchema?.type)).toEqual([
|
||||
"object",
|
||||
"object",
|
||||
]);
|
||||
const found = await client.callTool({
|
||||
name: "search_guidance",
|
||||
arguments: { query: "exact JavaScript module", limit: 1 },
|
||||
});
|
||||
expect(found.isError).not.toBe(true);
|
||||
const search = searchGuidanceToolOutputSchema.parse(
|
||||
found.structuredContent,
|
||||
).result;
|
||||
if (search.status !== "matches")
|
||||
throw new Error("Expected guidance matches");
|
||||
const locator = search.matches[0]?.locator;
|
||||
const read = await client.callTool({
|
||||
name: "read_guidance_section",
|
||||
arguments: { locator },
|
||||
});
|
||||
expect(
|
||||
readGuidanceSectionToolOutputSchema.parse(read.structuredContent)
|
||||
.result,
|
||||
).toMatchObject({ status: "section", locator });
|
||||
const none = await client.callTool({
|
||||
name: "search_guidance",
|
||||
arguments: { query: "qzxv-unlikely-guidance-term" },
|
||||
});
|
||||
expect(
|
||||
searchGuidanceToolOutputSchema.parse(none.structuredContent).result,
|
||||
).toMatchObject({ status: "no_matches", total_matches: 0 });
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -2,9 +2,9 @@ import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
||||
import {
|
||||
readGuidanceSectionOutputSchema,
|
||||
readGuidanceSectionToolOutputSchema,
|
||||
searchGuidanceInputSchema,
|
||||
searchGuidanceOutputSchema,
|
||||
searchGuidanceToolOutputSchema,
|
||||
} from "../../../src/contracts/guidanceContracts.js";
|
||||
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
|
||||
import { registerGuidanceTools } from "../../../src/server/registerGuidanceTools.js";
|
||||
@@ -15,16 +15,11 @@ const open: {
|
||||
server: EvidenceMcpServer;
|
||||
resources: ArtifactResourceScope;
|
||||
}[] = [];
|
||||
// Every successful guidance result is the advertised object with `result`.
|
||||
const guidanceOutput = <T>(
|
||||
schema: { parse(value: unknown): T },
|
||||
schema: { parse(value: unknown): { readonly result: T } },
|
||||
value: unknown,
|
||||
): T => {
|
||||
// MCP's legacy output-schema projection wraps root-level union schemas.
|
||||
// Newer transports pass the object unchanged.
|
||||
if (typeof value === "object" && value !== null && "result" in value)
|
||||
return schema.parse(value.result);
|
||||
return schema.parse(value);
|
||||
};
|
||||
): T => schema.parse(value).result;
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
open
|
||||
@@ -79,7 +74,7 @@ it("advertises strict provider-free contracts and returns packaged guide results
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const parsed = guidanceOutput(
|
||||
searchGuidanceOutputSchema,
|
||||
searchGuidanceToolOutputSchema,
|
||||
response.structuredContent,
|
||||
);
|
||||
expect(parsed).toMatchObject({
|
||||
@@ -97,7 +92,10 @@ it("advertises strict provider-free contracts and returns packaged guide results
|
||||
});
|
||||
expect(section.isError).not.toBe(true);
|
||||
expect(
|
||||
guidanceOutput(readGuidanceSectionOutputSchema, section.structuredContent),
|
||||
guidanceOutput(
|
||||
readGuidanceSectionToolOutputSchema,
|
||||
section.structuredContent,
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "section",
|
||||
heading: selected?.heading,
|
||||
@@ -108,7 +106,7 @@ it("advertises strict provider-free contracts and returns packaged guide results
|
||||
arguments: { query: "module", limit: 1 },
|
||||
});
|
||||
const firstResult = guidanceOutput(
|
||||
searchGuidanceOutputSchema,
|
||||
searchGuidanceToolOutputSchema,
|
||||
firstPage.structuredContent,
|
||||
);
|
||||
if (firstResult.status !== "matches" || firstResult.next_cursor === undefined)
|
||||
@@ -122,6 +120,6 @@ it("advertises strict provider-free contracts and returns packaged guide results
|
||||
},
|
||||
});
|
||||
expect(
|
||||
guidanceOutput(searchGuidanceOutputSchema, nextPage.structuredContent),
|
||||
guidanceOutput(searchGuidanceToolOutputSchema, nextPage.structuredContent),
|
||||
).toMatchObject({ status: "matches", offset: 1 });
|
||||
});
|
||||
Reference in new issue
Block a user