From 1581e40fce733c19e11b460b47722e4121e9fd0e Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 17 Jul 2026 02:22:04 +0000 Subject: [PATCH] fix(mcp): clarify advertised schema fields --- src/server/toolRegistrationOptions.ts | 121 +++++++++++++++++++- tests/contractSnapshot.test.ts | 26 +++++ tests/integration/serverIntegration.test.ts | 11 ++ 3 files changed, 155 insertions(+), 3 deletions(-) diff --git a/src/server/toolRegistrationOptions.ts b/src/server/toolRegistrationOptions.ts index d7e1b189a..5bf6a2788 100644 --- a/src/server/toolRegistrationOptions.ts +++ b/src/server/toolRegistrationOptions.ts @@ -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 => { + 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> = { + 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> => typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/tests/contractSnapshot.test.ts b/tests/contractSnapshot.test.ts index b8c9aeb97..d45ef3423 100644 --- a/tests/contractSnapshot.test.ts +++ b/tests/contractSnapshot.test.ts @@ -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, diff --git a/tests/integration/serverIntegration.test.ts b/tests/integration/serverIntegration.test.ts index 9465fcb82..ce16ebc9e 100644 --- a/tests/integration/serverIntegration.test.ts +++ b/tests/integration/serverIntegration.test.ts @@ -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`); } });