From b55084a5a01aa5319966a58268414bf10c952521 Mon Sep 17 00:00:00 2001 From: guitavano Date: Mon, 27 Jul 2026 10:35:34 -0300 Subject: [PATCH] fix(vtex): surface curated param descriptions to the agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated Zod schemas are produced with `metadata: false`, so no field description reaches the tool inputSchema (the JSON Schema the LLM sees). Agents got bare `f_RnB: string` with no hint of meaning or value format — which is why an agent couldn't map a coupon/promotion query to `f_RnB`. Rather than re-enabling metadata globally (measured: +1.57 MB / +35% server bundle, plus unrelated upstream schema drift and a broken export on regen), layer curated descriptions onto the flattened input schema in the tool adapter. Covers the params where the omission hurts on VTEX_LIST_ORDERS (f_RnB, date ranges, status, full-text q), written richer than the raw OpenAPI text — e.g. f_RnB now explains the coupon -> promotion-id lookup. Bundle unchanged (4.70 MB). Adds regression tests asserting descriptions reach the agent-facing JSON Schema and that overrides keep fields optional. Co-Authored-By: Claude Opus 4.8 --- vtex/server/lib/param-descriptions.ts | 67 +++++++++++++++++++++++++++ vtex/server/lib/tool-adapter.ts | 9 +++- vtex/server/tools/registry.test.ts | 41 +++++++++++++++- 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 vtex/server/lib/param-descriptions.ts diff --git a/vtex/server/lib/param-descriptions.ts b/vtex/server/lib/param-descriptions.ts new file mode 100644 index 00000000..e287cd5d --- /dev/null +++ b/vtex/server/lib/param-descriptions.ts @@ -0,0 +1,67 @@ +import { z } from "zod"; + +/** + * Curated parameter descriptions, applied on top of each tool's input schema. + * + * Why this exists: the generated Zod schemas are produced with + * `metadata: false` (see `openapi-ts.config.ts`), so NO field description + * reaches the tool `inputSchema` — i.e. the JSON Schema the agent/LLM sees. + * The agent therefore gets bare `f_RnB: string` with no hint of what it means + * or what value format it expects. + * + * Rather than re-enabling metadata globally (which inflates the server bundle + * by ~1.6 MB and would pull in unrelated upstream schema drift on regen), we + * curate descriptions for the params where the omission actually hurts: + * VTEX jargon (`RnB`), non-obvious value formats (date ranges), or closed + * value sets (status). These are richer than VTEX's raw OpenAPI text on + * purpose — e.g. `f_RnB` explains the coupon → promotion-id lookup that the + * raw "rates and benefits" wording omits. + * + * Keyed by tool id → field name. Adding coverage = one line here. + */ +export const PARAM_DESCRIPTIONS: Record> = { + VTEX_LIST_ORDERS: { + f_RnB: + "Filter orders by promotion (VTEX 'rates and benefits' / RnB). The value " + + "is the promotion's identifier — NOT the coupon code. To count sales for " + + "a coupon (e.g. 'FICA10') or a promotion name (e.g. 'Pop retenção DECO'), " + + "first resolve its promotion id using the promotions tools, then pass that " + + "id here.", + f_creationDate: + "Filter by order creation date. Format: " + + "`creationDate:[ TO ]` using UTC timestamps, e.g. " + + "`creationDate:[2026-07-25T00:00:00.000Z TO 2026-07-27T23:59:59.999Z]`.", + f_invoicedDate: + "Filter by invoiced date. Format: `invoicedDate:[ TO ]` using " + + "UTC timestamps, e.g. " + + "`invoicedDate:[2026-07-25T00:00:00.000Z TO 2026-07-27T23:59:59.999Z]`.", + f_status: + "Filter by order status. Valid values: " + + "waiting-for-sellers-confirmation, payment-pending, payment-approved, " + + "ready-for-handling, handling, invoiced, canceled.", + q: + "Full-text search over order id, client email, client document and " + + "client name. The `+` character is not allowed.", + }, +}; + +/** + * Return a copy of `schema` with curated `.describe()` metadata applied to any + * field listed for `toolId`. Fields with no override, and tools with no entry, + * are returned unchanged. Missing fields in the map are ignored (safe if the + * generated schema changes shape). + */ +export function applyParamDescriptions( + toolId: string, + schema: z.ZodObject, +): z.ZodObject { + const overrides = PARAM_DESCRIPTIONS[toolId]; + if (!overrides) return schema; + + const shape = schema.shape as Record; + const next: Record = { ...shape }; + for (const [field, description] of Object.entries(overrides)) { + if (next[field]) next[field] = next[field].describe(description); + } + return z.object(next); +} diff --git a/vtex/server/lib/tool-adapter.ts b/vtex/server/lib/tool-adapter.ts index 3bb2707f..fbabc8e4 100644 --- a/vtex/server/lib/tool-adapter.ts +++ b/vtex/server/lib/tool-adapter.ts @@ -10,6 +10,7 @@ import { createVtexClient, resolveCredentials, } from "./client-factory.ts"; +import { applyParamDescriptions } from "./param-descriptions.ts"; // ────────────────────────────────────────────────────────────────────────────── // Schema introspection helpers @@ -295,7 +296,13 @@ export interface ToolFromOperationConfig { * `createTool` definition that the MCP runtime can register. */ export function createToolFromOperation(config: ToolFromOperationConfig) { - const flatInput = flattenRequestSchema(config.requestSchema); + // Generated schemas carry no field descriptions (metadata: false), so layer + // curated ones onto the flattened input before it becomes the agent-facing + // inputSchema. + const flatInput = applyParamDescriptions( + config.id, + flattenRequestSchema(config.requestSchema), + ); // The factory's `env` is captured ONCE when the runtime resolves tool // registrations on the first request, then cached for the process lifetime diff --git a/vtex/server/tools/registry.test.ts b/vtex/server/tools/registry.test.ts index 9e635053..06e0104f 100644 --- a/vtex/server/tools/registry.test.ts +++ b/vtex/server/tools/registry.test.ts @@ -1,5 +1,10 @@ import { describe, test, expect, mock } from "bun:test"; -import { createToolFromOperation } from "../lib/tool-adapter.ts"; +import { z } from "zod"; +import { + createToolFromOperation, + flattenRequestSchema, +} from "../lib/tool-adapter.ts"; +import { applyParamDescriptions } from "../lib/param-descriptions.ts"; // ── Zod schemas ──────────────────────────────────────────────────────────────── import * as catalogZod from "../generated/catalog/zod.gen.ts"; @@ -1140,6 +1145,40 @@ describe("VTEX_LIST_ORDERS", () => { }); }); +// ────────────────────────────────────────────────────────────────────────────── +// Curated param descriptions +// ────────────────────────────────────────────────────────────────────────────── + +describe("param descriptions", () => { + test("VTEX_LIST_ORDERS: curated descriptions reach the agent-facing JSON Schema", () => { + const flat = applyParamDescriptions( + "VTEX_LIST_ORDERS", + flattenRequestSchema(ordersZod.zListOrdersData as any), + ); + const json: any = z.toJSONSchema(flat, { + io: "input", + unrepresentable: "any", + }); + + // f_RnB must explain the coupon → promotion-id lookup (the reported gap). + expect(json.properties?.f_RnB?.description).toMatch(/promotion id/i); + expect(json.properties?.f_RnB?.description).toMatch(/coupon/i); + // Value-format hints for date filters. + expect(json.properties?.f_creationDate?.description).toMatch( + /creationDate:\[/, + ); + // Overriding a field must not make it required. + expect((json.required ?? []).includes("f_RnB")).toBe(false); + }); + + test("tools without curated overrides are returned unchanged", () => { + const schema = flattenRequestSchema( + catalogZod.zGetApiCatalogPvtBrandByBrandIdData as any, + ); + expect(applyParamDescriptions("VTEX_GET_BRAND", schema)).toBe(schema); + }); +}); + // ────────────────────────────────────────────────────────────────────────────── // Collection // ──────────────────────────────────────────────────────────────────────────────