fix(mcp): clarify advertised schema fields
This commit is contained in:
parent
a7c1e87365
commit
1581e40fce
|
|
@ -41,9 +41,14 @@ const advertisedOutputSchema = (schema: z.ZodObject) => {
|
|||
"~standard": {
|
||||
...standard,
|
||||
jsonSchema: {
|
||||
input: () => closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
input: () =>
|
||||
describeRootProperties(
|
||||
closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
),
|
||||
output: () =>
|
||||
closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
describeRootProperties(
|
||||
closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -93,7 +98,7 @@ const describeProperties = (
|
|||
isObject(schema) && typeof schema.description !== "string"
|
||||
? {
|
||||
...describeProperties(schema),
|
||||
description: property.replaceAll("_", " "),
|
||||
description: fallbackPropertyDescription(property),
|
||||
}
|
||||
: describeValue(schema),
|
||||
]),
|
||||
|
|
@ -107,5 +112,115 @@ const describeValue = (value: unknown): unknown => {
|
|||
return isObject(value) ? describeProperties(value) : value;
|
||||
};
|
||||
|
||||
const describeRootProperties = (
|
||||
value: Readonly<Record<string, unknown>>,
|
||||
): Record<string, unknown> => {
|
||||
const properties = value.properties;
|
||||
if (!isObject(properties)) return { ...value };
|
||||
return {
|
||||
...value,
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(properties).map(([property, schema]) => [
|
||||
property,
|
||||
isObject(schema) && typeof schema.description !== "string"
|
||||
? {
|
||||
...schema,
|
||||
description: fallbackPropertyDescription(property),
|
||||
}
|
||||
: schema,
|
||||
]),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const PROPERTY_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
||||
addresses: "Ordered provider-normalized procedure addresses to analyze.",
|
||||
approved: "Explicit operator approval to perform this operation.",
|
||||
after: "The later or right-hand observation to compare.",
|
||||
before: "The earlier or left-hand observation to compare.",
|
||||
boundary_id: "Exact reconstruction boundary identifier to evaluate.",
|
||||
case_sensitive:
|
||||
"Whether text matching distinguishes uppercase and lowercase.",
|
||||
cdp_endpoint: "Approved literal loopback Chrome DevTools Protocol endpoint.",
|
||||
comment: "Exact analyst comment text to write.",
|
||||
comparisons: "Validated comparison Evidence records to aggregate.",
|
||||
coverage: "Exact reconstruction-coverage commitment to verify.",
|
||||
detail: "Requested response detail level.",
|
||||
direction: "Direction in which to traverse or compare relationships.",
|
||||
document: "Exact provider document or program identity.",
|
||||
error: "Structured, caller-actionable error when the operation fails.",
|
||||
evidence: "Evidence v2 record produced by this operation.",
|
||||
evidence_id: "Stable identifier of the recorded Evidence v2 observation.",
|
||||
evidence_uri:
|
||||
"Session resource URI for the recorded Evidence v2 observation.",
|
||||
executable: "Approved absolute path of the executable to run.",
|
||||
format: "Declared input artifact format.",
|
||||
name: "Exact name used by this operation.",
|
||||
pattern: "Text or bounded pattern used to filter matching results.",
|
||||
phase: "Current plan or execution phase of the operation.",
|
||||
plan: "Content-bound execution plan and its approval commitment.",
|
||||
query: "Non-empty feature or text query to investigate.",
|
||||
question: "Concrete unresolved question to retain for later investigation.",
|
||||
result: "Primary structured result returned by this operation.",
|
||||
left: "Left-hand input used for comparison or differential execution.",
|
||||
limit: "Maximum number of results to return in this page.",
|
||||
limits:
|
||||
"Bounded resource-consumption and result-size limits for this operation.",
|
||||
mode: "Operation mode that selects the requested behavior.",
|
||||
offset: "Zero-based index of the first result to return.",
|
||||
overwrite: "Whether an existing destination may be replaced.",
|
||||
path: "Local filesystem path used by this operation.",
|
||||
provider_id:
|
||||
"Exact deep-analysis provider ID, or automatic selection when omitted.",
|
||||
right: "Right-hand input used for comparison or differential execution.",
|
||||
schema_version: "Version of this structured result schema.",
|
||||
source_evidence:
|
||||
"Evidence records supporting the prepared source transformation.",
|
||||
status: "Current lifecycle or verification status.",
|
||||
summary: "Concise evidence-backed summary of the result.",
|
||||
symbols: "Ordered bounded Swift symbols to demangle.",
|
||||
target_id: "Exact authorized CDP target identifier.",
|
||||
unknown_id: "Exact residual-unknown identifier.",
|
||||
unknown_registry_approved:
|
||||
"Explicit approval to record bounded residual uncertainty in the session registry.",
|
||||
workspace_path:
|
||||
"Approved local path of the persistent investigation workspace.",
|
||||
};
|
||||
|
||||
const fallbackPropertyDescription = (property: string): string => {
|
||||
const explicit = PROPERTY_DESCRIPTIONS[property];
|
||||
if (explicit !== undefined) return explicit;
|
||||
const words = property.replaceAll("_", " ");
|
||||
if (property.endsWith("_approved"))
|
||||
return `Explicit operator approval for ${words.slice(0, -9)}.`;
|
||||
if (property.endsWith("_offset"))
|
||||
return `Zero-based index of the first ${words.slice(0, -7)} to return.`;
|
||||
if (property.endsWith("_limit"))
|
||||
return `Maximum number of ${words.slice(0, -6)} entries to return.`;
|
||||
if (property.startsWith("max_"))
|
||||
return `Maximum permitted ${words.slice(4)} for this operation.`;
|
||||
if (property.startsWith("include_"))
|
||||
return `Whether to include ${words.slice(8)} in the result.`;
|
||||
if (property.startsWith("expected_"))
|
||||
return `Expected ${words.slice(9)} used to reject stale or mismatched input.`;
|
||||
if (property.endsWith("_sha256"))
|
||||
return `Exact SHA-256 digest of ${words.slice(0, -7)}.`;
|
||||
if (property.endsWith("_evidence_id"))
|
||||
return `Exact Evidence v2 identifier for the ${words.slice(0, -12)} observation.`;
|
||||
if (property.endsWith("_evidence_ids"))
|
||||
return `Ordered Evidence v2 identifiers for the ${words.slice(0, -13)} observations.`;
|
||||
if (property.endsWith("_path"))
|
||||
return `Local filesystem path for ${words.slice(0, -5)}.`;
|
||||
if (property.endsWith("_uri"))
|
||||
return `Canonical URI for ${words.slice(0, -4)}.`;
|
||||
if (property.endsWith("_bytes"))
|
||||
return `Bounded byte count for ${words.slice(0, -6)}.`;
|
||||
if (property.endsWith("_root") || property.endsWith("_roots"))
|
||||
return `Approved canonical filesystem ${words}.`;
|
||||
if (property.startsWith("is_") || property.startsWith("has_"))
|
||||
return `Whether ${words}.`;
|
||||
return `Value for ${words}.`;
|
||||
};
|
||||
|
||||
const isObject = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
annotationsFromEffects,
|
||||
TOOL_EFFECTS,
|
||||
} from "../src/contracts/toolEffects.js";
|
||||
import { toolRegistrationOptions } from "../src/server/toolRegistrationOptions.js";
|
||||
|
||||
const convertContractJsonSchema = (schema: z.ZodType) =>
|
||||
z.toJSONSchema(schema, {
|
||||
|
|
@ -59,6 +60,31 @@ const emptySchemaPaths = (value: unknown, path = "$"): string[] => {
|
|||
};
|
||||
|
||||
describe("tool contract surface", () => {
|
||||
it("gives fallback parameter guidance operational meaning", () => {
|
||||
const contract = TOOL_CONTRACTS.find(
|
||||
({ name }) => name === "analyze_function",
|
||||
);
|
||||
expect(contract).toBeDefined();
|
||||
if (contract === undefined) return;
|
||||
|
||||
const advertisedSchema =
|
||||
toolRegistrationOptions(contract).inputSchema[
|
||||
"~standard"
|
||||
].jsonSchema.input();
|
||||
const properties = Object.fromEntries(
|
||||
Object.entries(advertisedSchema.properties ?? {}),
|
||||
);
|
||||
expect(properties.include_assembly).toMatchObject({
|
||||
description: "Whether to include assembly in the result.",
|
||||
});
|
||||
expect(properties.max_instructions).toMatchObject({
|
||||
description: "Maximum permitted instructions for this operation.",
|
||||
});
|
||||
expect(properties.pseudocode_offset).toMatchObject({
|
||||
description: "Zero-based index of the first pseudocode to return.",
|
||||
});
|
||||
});
|
||||
|
||||
it("advertises complete typed schemas and annotations for all analysis tools", () => {
|
||||
const contracts = [
|
||||
...OFFICIAL_TOOL_CONTRACTS,
|
||||
|
|
|
|||
|
|
@ -115,6 +115,16 @@ const assertDescribedStrictObjects = (
|
|||
assertDescribedStrictObjects(value, root, `${path}[${String(index)}]`);
|
||||
};
|
||||
|
||||
const assertDescribedRootObject = (schema: unknown, path: string): void => {
|
||||
const object = Object.fromEntries(objectEntries(schema));
|
||||
expect(object.additionalProperties, path).toBe(false);
|
||||
for (const [property, propertySchema] of objectEntries(object.properties))
|
||||
expect(
|
||||
Object.fromEntries(objectEntries(propertySchema)).description,
|
||||
`${path}.properties.${property}`,
|
||||
).toEqual(expect.any(String));
|
||||
};
|
||||
|
||||
describe("full MCP integration with multi-tool sequences", () => {
|
||||
it("executes a realistic workflow: list methods, decompile selected, get xrefs", async () => {
|
||||
const client = await connect({
|
||||
|
|
@ -225,6 +235,7 @@ describe("full MCP integration with multi-tool sequences", () => {
|
|||
tool.inputSchema,
|
||||
tool.name,
|
||||
);
|
||||
assertDescribedRootObject(tool.outputSchema, `${tool.name}.output`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue