Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions vtex/server/lib/param-descriptions.ts
Original file line number Diff line number Diff line change
@@ -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<string, Record<string, string>> = {
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:[<from> TO <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:[<from> TO <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<any>,
): z.ZodObject<any> {
const overrides = PARAM_DESCRIPTIONS[toolId];
if (!overrides) return schema;

const shape = schema.shape as Record<string, z.ZodTypeAny>;
const next: Record<string, z.ZodTypeAny> = { ...shape };
for (const [field, description] of Object.entries(overrides)) {
if (next[field]) next[field] = next[field].describe(description);
}
return z.object(next);
}
9 changes: 8 additions & 1 deletion vtex/server/lib/tool-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
createVtexClient,
resolveCredentials,
} from "./client-factory.ts";
import { applyParamDescriptions } from "./param-descriptions.ts";

// ──────────────────────────────────────────────────────────────────────────────
// Schema introspection helpers
Expand Down Expand Up @@ -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
Expand Down
41 changes: 40 additions & 1 deletion vtex/server/tools/registry.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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
// ──────────────────────────────────────────────────────────────────────────────
Expand Down
Loading