From 372433b683e39eeb6fd7a88ec1fd1ea8e6310ed8 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 29 Jan 2026 15:04:10 -0300 Subject: [PATCH 1/7] refactor: add list/listPaginated pattern and reorganize schemas - Add list (simple) + listPaginated (cursor-based) for customer, order, checkout - Move entity schemas from contracts/ to schemas/: - ProductSchema, ProductDetailSchema, ProductPriceSchema -> schemas/product.ts - CheckoutStatusSchema, CheckoutTypeSchema, CheckoutListItemSchema -> schemas/checkout.ts - OrderWithRelationsSchema -> schemas/order.ts - Create shared PaginatedInputSchema for DRY pagination inputs - Add externalId to CreateCustomerInput and UpdateCustomerInput --- src/contracts/checkout.ts | 160 +++++++++++++++----------------------- src/contracts/customer.ts | 76 +++++++++++++----- src/contracts/order.ts | 48 ++++++++---- src/contracts/products.ts | 120 +++++++++++++++++++++------- src/index.ts | 99 +++++++++++++++++++++-- src/schemas/checkout.ts | 41 ++++++++++ src/schemas/order.ts | 13 ++++ src/schemas/pagination.ts | 20 +++++ src/schemas/product.ts | 29 +++++-- 9 files changed, 430 insertions(+), 176 deletions(-) diff --git a/src/contracts/checkout.ts b/src/contracts/checkout.ts index 5f7262b..7ff7c3d 100644 --- a/src/contracts/checkout.ts +++ b/src/contracts/checkout.ts @@ -1,16 +1,30 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; -import { CheckoutSchema } from "../schemas/checkout"; -import { CurrencySchema } from "../schemas/currency"; -import { CustomerSchema } from "../schemas/customer"; import { - PaginationInputSchema, - PaginationOutputSchema, -} from "../schemas/pagination"; + CheckoutSchema, + CheckoutStatusSchema, + CheckoutTypeSchema, + CheckoutListItemSchema, + CheckoutDetailSchema, + type CheckoutStatus, + type CheckoutType, + type CheckoutListItem, + type CheckoutDetail, +} from "../schemas/checkout"; +import { CurrencySchema } from "../schemas/currency"; +import { PaginatedInputSchema, PaginationOutputSchema } from "../schemas/pagination"; + +// Re-export entity schemas for backwards compatibility +export { + CheckoutStatusSchema, + CheckoutTypeSchema, + CheckoutListItemSchema, + CheckoutDetailSchema, +}; +export type { CheckoutStatus, CheckoutType, CheckoutListItem, CheckoutDetail }; /** * Helper to treat empty strings as undefined (not provided). - * This allows clients to pass empty strings without validation errors. */ const emptyStringToUndefined = z .string() @@ -23,22 +37,12 @@ const emailOrEmpty = z.string().email().optional().or(z.literal("")); /** * Valid fields that can be required at checkout time. - * - Standard fields: 'email', 'name' (checked against customer.email/name) - * - Any other string is a custom field (checked against customer[field]) - * - * @example ['email'] - require email - * @example ['email', 'name'] - require both email and name - * @example ['email', 'company'] - require email and company */ export const CustomerFieldSchema = z.string().min(1); export type CustomerField = string; /** - * Customer data object for checkout. - * Flat structure - standard fields (name, email, externalId) plus any custom string fields. - * Empty strings are treated as undefined (not provided). - * - * @example { name: "John", email: "john@example.com", externalId: "user_123", company: "Acme" } + * Customer data object for checkout input. */ export const CustomerInputSchema = z .object({ @@ -50,6 +54,7 @@ export const CustomerInputSchema = z export type CustomerInput = z.infer; +// Input schemas export const CreateCheckoutInputSchema = z.object({ nodeId: z.string(), amount: z.number().optional(), @@ -58,33 +63,13 @@ export const CreateCheckoutInputSchema = z.object({ successUrl: z.string().optional(), allowDiscountCodes: z.boolean().optional(), metadata: z.record(z.string(), z.any()).optional(), - /** - * Customer data for this checkout. - */ customer: CustomerInputSchema.optional(), - /** - * Array of customer fields to require at checkout. - * If a field is listed here and not provided, the checkout UI will prompt for it. - * @example ['email'] - require email - * @example ['email', 'name'] - require both - */ requireCustomerData: z.array(CustomerFieldSchema).optional(), }); export const ConfirmCheckoutInputSchema = z.object({ checkoutId: z.string(), - /** - * Customer data provided at confirm time. - */ customer: CustomerInputSchema.optional(), - /** - * Product selection at confirm time. - * - undefined or [] = keep current selection - * - [{ productId }] = change selection to this product - * - priceAmount required if selected price has amountType: CUSTOM - * - * Currently limited to single selection (max 1 item). - */ products: z .array( z.object({ @@ -120,25 +105,54 @@ export const PaymentReceivedInputSchema = z.object({ ), }); -export const GetCheckoutInputSchema = z.object({ id: z.string() }); +export const GetCheckoutInputSchema = z.object({ + id: z.string().describe("The checkout ID"), +}); +export type GetCheckoutInput = z.infer; export type CreateCheckout = z.infer; export type ConfirmCheckout = z.infer; export type RegisterInvoice = z.infer; export type PaymentReceived = z.infer; +// List output schemas +export const ListCheckoutsOutputSchema = z.object({ + checkouts: z.array(CheckoutSchema), +}); +export type ListCheckoutsOutput = z.infer; + +export const ListCheckoutsPaginatedInputSchema = PaginatedInputSchema.extend({ + status: CheckoutStatusSchema.optional().describe("Filter by status: UNCONFIRMED, CONFIRMED, PENDING_PAYMENT, PAYMENT_RECEIVED, or EXPIRED"), +}); +export type ListCheckoutsPaginatedInput = z.infer; + +export const ListCheckoutsPaginatedOutputSchema = PaginationOutputSchema.extend({ + checkouts: z.array(CheckoutSchema), +}); +export type ListCheckoutsPaginatedOutput = z.infer; + +export const ListCheckoutsSummaryOutputSchema = PaginationOutputSchema.extend({ + checkouts: z.array(CheckoutListItemSchema), +}); +export type ListCheckoutsSummaryOutput = z.infer; + +// Contracts export const createCheckoutContract = oc .input(CreateCheckoutInputSchema) .output(CheckoutSchema); + export const applyDiscountCodeContract = oc .input(ApplyDiscountCodeInputSchema) .output(CheckoutSchema); + export const confirmCheckoutContract = oc .input(ConfirmCheckoutInputSchema) .output(CheckoutSchema); + export const registerInvoiceContract = oc .input(RegisterInvoiceInputSchema) .output(CheckoutSchema); + export const getCheckoutContract = oc .input(GetCheckoutInputSchema) .output(CheckoutSchema); @@ -147,67 +161,19 @@ export const paymentReceivedContract = oc .input(PaymentReceivedInputSchema) .output(z.object({ ok: z.boolean() })); -// List checkouts schemas -export const CheckoutStatusSchema = z.enum([ - "UNCONFIRMED", - "CONFIRMED", - "PENDING_PAYMENT", - "PAYMENT_RECEIVED", - "EXPIRED", -]); -export type CheckoutStatus = z.infer; - -export const CheckoutTypeSchema = z.enum(["PRODUCTS", "AMOUNT", "TOP_UP"]); -export type CheckoutType = z.infer; - -const ListCheckoutsInputSchema = PaginationInputSchema.extend({ - status: CheckoutStatusSchema.optional(), -}); - -const ListCheckoutsOutputSchema = PaginationOutputSchema.extend({ - checkouts: z.array(CheckoutSchema), -}); - export const listCheckoutsContract = oc - .input(ListCheckoutsInputSchema) + .input(z.object({})) .output(ListCheckoutsOutputSchema); -const CheckoutCustomerSchema = CustomerSchema.nullable(); - -// MCP-specific summary schema for list (simpler than full CheckoutSchema) -const CheckoutListItemSchema = z.object({ - id: z.string(), - status: CheckoutStatusSchema, - type: CheckoutTypeSchema, - currency: CurrencySchema, - totalAmount: z.number().nullable(), - customerId: z.string().nullable(), - customer: CheckoutCustomerSchema, - productId: z.string().nullable(), - organizationId: z.string(), - expiresAt: z.date(), - createdAt: z.date(), - modifiedAt: z.date().nullable(), -}); - -// MCP-specific detailed schema for get (includes additional fields) -const CheckoutDetailSchema = CheckoutListItemSchema.extend({ - userMetadata: z.record(z.unknown()).nullable(), - successUrl: z.string().nullable(), - discountAmount: z.number().nullable(), - netAmount: z.number().nullable(), - taxAmount: z.number().nullable(), -}); - -const ListCheckoutsSummaryOutputSchema = PaginationOutputSchema.extend({ - checkouts: z.array(CheckoutListItemSchema), -}); +export const listCheckoutsPaginatedContract = oc + .input(ListCheckoutsPaginatedInputSchema) + .output(ListCheckoutsPaginatedOutputSchema); -export const listCheckoutsSummaryContract = oc - .input(ListCheckoutsInputSchema) +export const listCheckoutsSummaryPaginatedContract = oc + .input(ListCheckoutsPaginatedInputSchema) .output(ListCheckoutsSummaryOutputSchema); -export const getCheckoutSummaryContract = oc +export const getCheckoutDetailContract = oc .input(GetCheckoutInputSchema) .output(CheckoutDetailSchema); @@ -218,6 +184,8 @@ export const checkout = { registerInvoice: registerInvoiceContract, paymentReceived: paymentReceivedContract, list: listCheckoutsContract, - listSummary: listCheckoutsSummaryContract, - getSummary: getCheckoutSummaryContract, + listPaginated: listCheckoutsPaginatedContract, + // Original names preserved + listSummary: listCheckoutsSummaryPaginatedContract, + getSummary: getCheckoutDetailContract, }; diff --git a/src/contracts/customer.ts b/src/contracts/customer.ts index 77f21a6..c5b5f18 100644 --- a/src/contracts/customer.ts +++ b/src/contracts/customer.ts @@ -5,45 +5,78 @@ import { CustomerWithSubscriptionsSchema, GetCustomerInputSchema as SdkGetCustomerInputSchema, } from "../schemas/customer"; -import { - PaginationInputSchema, - PaginationOutputSchema, -} from "../schemas/pagination"; +import { PaginatedInputSchema, PaginationOutputSchema } from "../schemas/pagination"; + +// Simple list (no pagination) +export const ListCustomersOutputSchema = z.object({ + customers: z.array(CustomerSchema), +}); +export type ListCustomersOutput = z.infer; + +// Paginated list (no additional filters for customers) +export const ListCustomersPaginatedInputSchema = PaginatedInputSchema; +export type ListCustomersPaginatedInput = z.infer; -// MCP-specific schemas -const ListCustomersInputSchema = PaginationInputSchema; -const ListCustomersOutputSchema = PaginationOutputSchema.extend({ +export const ListCustomersPaginatedOutputSchema = PaginationOutputSchema.extend({ customers: z.array(CustomerSchema), }); +export type ListCustomersPaginatedOutput = z.infer; -const McpGetCustomerInputSchema = z.object({ id: z.string() }); +// Flexible customer lookup - exactly one of id, email, or externalId +// Base shape without refinement (for MCP tool schemas) +export const CustomerLookupBaseSchema = z.object({ + id: z.string().optional().describe("The customer ID"), + email: z.string().optional().describe("The customer email address"), + externalId: z.string().optional().describe("The external ID from your system"), +}); + +// With refinement for runtime validation +export const CustomerLookupInputSchema = CustomerLookupBaseSchema.refine( + (data) => [data.id, data.email, data.externalId].filter(Boolean).length === 1, + { message: "Exactly one of id, email, or externalId must be provided" }, +); +export type CustomerLookupInput = z.infer; + +// Aliases for specific operations +export const GetCustomerInputSchema = CustomerLookupBaseSchema; +export type GetCustomerInput = z.infer; -const CreateCustomerInputSchema = z.object({ - name: z.string().min(1), - email: z.string().email(), +export const DeleteCustomerInputSchema = CustomerLookupBaseSchema; +export type DeleteCustomerInput = z.infer; + +export const CreateCustomerInputSchema = z.object({ + name: z.string().min(1).describe("Customer name"), + email: z.string().email().describe("Customer email address"), + externalId: z.string().optional().describe("External ID from your system for linking"), }); -const UpdateCustomerInputSchema = z.object({ - id: z.string(), - name: z.string().optional(), - email: z.string().email().optional(), - userMetadata: z.record(z.string(), z.string()).optional(), +export const UpdateCustomerInputSchema = z.object({ + id: z.string().describe("The customer ID to update"), + name: z.string().optional().describe("New customer name"), + email: z.string().email().optional().describe("New customer email address"), + externalId: z.string().optional().describe("External ID from your system for linking"), + userMetadata: z.record(z.string(), z.string()).optional().describe("Custom metadata key-value pairs"), }); -const DeleteCustomerInputSchema = z.object({ id: z.string() }); +export type CreateCustomerInput = z.infer; +export type UpdateCustomerInput = z.infer; // SDK contract - uses flexible lookup (externalId/email/customerId) export const getSdkCustomerContract = oc .input(SdkGetCustomerInputSchema) .output(CustomerWithSubscriptionsSchema); -// MCP contracts +// Contracts export const listCustomersContract = oc - .input(ListCustomersInputSchema) + .input(z.object({})) .output(ListCustomersOutputSchema); +export const listCustomersPaginatedContract = oc + .input(ListCustomersPaginatedInputSchema) + .output(ListCustomersPaginatedOutputSchema); + export const getCustomerContract = oc - .input(McpGetCustomerInputSchema) + .input(GetCustomerInputSchema) .output(CustomerSchema); export const createCustomerContract = oc @@ -56,10 +89,11 @@ export const updateCustomerContract = oc export const deleteCustomerContract = oc .input(DeleteCustomerInputSchema) - .output(z.object({ ok: z.literal(true) })); + .output(z.void()); export const customer = { list: listCustomersContract, + listPaginated: listCustomersPaginatedContract, get: getCustomerContract, getSdk: getSdkCustomerContract, create: createCustomerContract, diff --git a/src/contracts/order.ts b/src/contracts/order.ts index 6ccdf8d..3503834 100644 --- a/src/contracts/order.ts +++ b/src/contracts/order.ts @@ -1,36 +1,52 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; -import { CustomerSchema } from "../schemas/customer"; -import { OrderItemSchema, OrderSchema } from "../schemas/order"; import { - PaginationInputSchema, - PaginationOutputSchema, -} from "../schemas/pagination"; - -// Order with related data for list and get views -const OrderWithRelationsSchema = OrderSchema.extend({ - customer: CustomerSchema.nullable(), - orderItems: z.array(OrderItemSchema), + OrderWithRelationsSchema, + type OrderWithRelations, +} from "../schemas/order"; +import { PaginatedInputSchema, PaginationOutputSchema } from "../schemas/pagination"; + +// Re-export entity schema for backwards compatibility +export { OrderWithRelationsSchema }; +export type { OrderWithRelations }; + +// List output schemas +export const ListOrdersOutputSchema = z.object({ + orders: z.array(OrderWithRelationsSchema), }); +export type ListOrdersOutput = z.infer; -const ListOrdersInputSchema = PaginationInputSchema.extend({ - customerId: z.string().optional(), - status: z.string().optional(), // Prisma uses String type for status +export const ListOrdersPaginatedInputSchema = PaginatedInputSchema.extend({ + customerId: z.string().optional().describe("Filter by customer ID"), + status: z.string().optional().describe("Filter by status: PENDING, PAID, REFUNDED, or CANCELLED"), }); +export type ListOrdersPaginatedInput = z.infer; -const ListOrdersOutputSchema = PaginationOutputSchema.extend({ +export const ListOrdersPaginatedOutputSchema = PaginationOutputSchema.extend({ orders: z.array(OrderWithRelationsSchema), }); +export type ListOrdersPaginatedOutput = z.infer; +export const GetOrderInputSchema = z.object({ + id: z.string().describe("The order ID"), +}); +export type GetOrderInput = z.infer; + +// Contracts export const listOrdersContract = oc - .input(ListOrdersInputSchema) + .input(z.object({})) .output(ListOrdersOutputSchema); +export const listOrdersPaginatedContract = oc + .input(ListOrdersPaginatedInputSchema) + .output(ListOrdersPaginatedOutputSchema); + export const getOrderContract = oc - .input(z.object({ id: z.string() })) + .input(GetOrderInputSchema) .output(OrderWithRelationsSchema); export const order = { list: listOrdersContract, + listPaginated: listOrdersPaginatedContract, get: getOrderContract, }; diff --git a/src/contracts/products.ts b/src/contracts/products.ts index d25359e..dc7c7d8 100644 --- a/src/contracts/products.ts +++ b/src/contracts/products.ts @@ -1,46 +1,62 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; import { CurrencySchema } from "../schemas/currency"; -import { ProductPriceInputSchema } from "../schemas/product-price-input"; +import { PaginatedInputSchema, PaginationOutputSchema } from "../schemas/pagination"; +import { + PriceAmountTypeSchema, + ProductPriceInputSchema, + RecurringIntervalInputSchema, +} from "../schemas/product-price-input"; +import { + ProductSchema, + ProductDetailSchema, + ProductPriceSchema, + type Product, + type ProductDetail, + type ProductPrice, +} from "../schemas/product"; -export const ProductPriceSchema = z.object({ - id: z.string(), - amountType: z.enum(["FIXED", "CUSTOM"]), - priceAmount: z.number().nullable(), - currency: CurrencySchema, -}); - -// Products have a prices array to allow future support of metered pricing -// (e.g., base subscription + usage-based charges). Currently only one static price -// (FIXED/CUSTOM) is supported. -export const ProductSchema = z.object({ - id: z.string(), - name: z.string(), - description: z.string().nullable(), - recurringInterval: z.enum(["MONTH", "QUARTER", "YEAR"]).nullable(), - prices: z.array(ProductPriceSchema), -}); +// Re-export entity schemas for backwards compatibility +export { + ProductSchema, + ProductDetailSchema, + ProductPriceSchema, +}; +export type { Product, ProductDetail, ProductPrice }; +// List output schemas export const ListProductsOutputSchema = z.object({ products: z.array(ProductSchema), }); +export type ListProductsOutput = z.infer; -export type Product = z.infer; -export type ProductPrice = z.infer; +export const ListProductsDetailOutputSchema = PaginationOutputSchema.extend({ + products: z.array(ProductDetailSchema), +}); +export type ListProductsDetailOutput = z.infer; +// Simple list without pagination export const listProductsContract = oc - .input(z.object({}).optional()) + .input(z.object({})) .output(ListProductsOutputSchema); +// Paginated list with full product details +export const ListProductsInputSchema = PaginatedInputSchema; +export type ListProductsInput = z.infer; + +export const listProductsPaginatedContract = oc + .input(ListProductsInputSchema) + .output(ListProductsDetailOutputSchema); + // CRUD input schemas -const CreateProductInputSchema = z.object({ +export const CreateProductInputSchema = z.object({ name: z.string().min(1), description: z.string().optional(), price: ProductPriceInputSchema, userMetadata: z.record(z.string(), z.string()).optional(), }); -const UpdateProductInputSchema = z.object({ +export const UpdateProductInputSchema = z.object({ id: z.string(), name: z.string().min(1).optional(), description: z.string().optional(), @@ -48,24 +64,70 @@ const UpdateProductInputSchema = z.object({ userMetadata: z.record(z.string(), z.string()).optional(), }); +export type CreateProductInput = z.infer; +export type UpdateProductInput = z.infer; + +// Flattened tool input schemas (flat params are easier for AI tools) +export const CreateProductToolInputSchema = z.object({ + name: z.string().min(1).describe("Product name"), + description: z.string().optional().describe("Product description"), + priceAmount: z.number().optional().describe( + "Price amount (in cents for USD, whole sats for SAT). Required for fixed pricing." + ), + currency: CurrencySchema.optional().describe("Currency: USD or SAT (default: USD)"), + amountType: PriceAmountTypeSchema.optional().describe("Amount type: FIXED or CUSTOM (default: FIXED)"), + recurringInterval: RecurringIntervalInputSchema.optional().describe( + "Recurring interval: NEVER (one-time), MONTH, QUARTER, or YEAR (default: NEVER)" + ), +}); + +export const UpdateProductToolInputSchema = z.object({ + id: z.string().describe("The product ID to update"), + name: z.string().optional().describe("New product name"), + description: z.string().optional().describe("New product description"), + priceAmount: z.number().optional().describe( + "New price amount (in cents for USD, whole sats for SAT)" + ), + currency: CurrencySchema.optional().describe("Currency: USD or SAT"), + amountType: PriceAmountTypeSchema.optional().describe("Amount type: FIXED or CUSTOM"), + recurringInterval: RecurringIntervalInputSchema.optional().describe( + "Recurring interval: NEVER, MONTH, QUARTER, or YEAR" + ), +}); + +export type CreateProductToolInput = z.infer; +export type UpdateProductToolInput = z.infer; + +export const GetProductInputSchema = z.object({ + id: z.string().describe("The product ID"), +}); +export type GetProductInput = z.infer; + +export const DeleteProductInputSchema = z.object({ + id: z.string().describe("The product ID to delete"), +}); +export type DeleteProductInput = z.infer; + +// Contracts export const getProductContract = oc - .input(z.object({ id: z.string() })) - .output(ProductSchema); + .input(GetProductInputSchema) + .output(ProductDetailSchema); export const createProductContract = oc .input(CreateProductInputSchema) - .output(ProductSchema); + .output(ProductDetailSchema); export const updateProductContract = oc .input(UpdateProductInputSchema) - .output(ProductSchema); + .output(ProductDetailSchema); export const deleteProductContract = oc - .input(z.object({ id: z.string() })) - .output(z.object({ ok: z.literal(true) })); + .input(DeleteProductInputSchema) + .output(z.void()); export const products = { list: listProductsContract, + listPaginated: listProductsPaginatedContract, get: getProductContract, create: createProductContract, update: updateProductContract, diff --git a/src/index.ts b/src/index.ts index 179e042..d4a6ffa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,13 @@ import { subscription } from "./contracts/subscription"; export type { CheckoutStatus, CheckoutType, + CheckoutListItem, + CheckoutDetail, + ListCheckoutsOutput, + ListCheckoutsPaginatedInput, + ListCheckoutsPaginatedOutput, + ListCheckoutsSummaryOutput, + GetCheckoutInput, ConfirmCheckout, CreateCheckout, PaymentReceived, @@ -16,6 +23,13 @@ export type { export { CheckoutStatusSchema, CheckoutTypeSchema, + CheckoutListItemSchema, + CheckoutDetailSchema, + ListCheckoutsOutputSchema, + ListCheckoutsPaginatedInputSchema, + ListCheckoutsPaginatedOutputSchema, + ListCheckoutsSummaryOutputSchema, + GetCheckoutInputSchema, } from "./contracts/checkout"; export type { BootstrapOnboarding, @@ -31,16 +45,59 @@ export type { CreateRenewalCheckout, GetSubscriptionInput, } from "./contracts/subscription"; -export type { GetCustomerInput } from "./schemas/customer"; +export type { GetCustomerInput as SdkGetCustomerInput } from "./schemas/customer"; +export type { + CreateCustomerInput, + UpdateCustomerInput, + ListCustomersOutput, + ListCustomersPaginatedInput, + ListCustomersPaginatedOutput, + GetCustomerInput, + DeleteCustomerInput, + CustomerLookupInput, +} from "./contracts/customer"; +export { + CreateCustomerInputSchema, + UpdateCustomerInputSchema, + ListCustomersOutputSchema, + ListCustomersPaginatedInputSchema, + ListCustomersPaginatedOutputSchema, + GetCustomerInputSchema, + DeleteCustomerInputSchema, + CustomerLookupInputSchema, + CustomerLookupBaseSchema, +} from "./contracts/customer"; export type { Checkout } from "./schemas/checkout"; export { CheckoutSchema } from "./schemas/checkout"; export type { Currency } from "./schemas/currency"; export { CurrencySchema } from "./schemas/currency"; -export type { Product, ProductPrice } from "./contracts/products"; +export type { + Product, + ProductDetail, + ProductPrice, + ListProductsOutput, + ListProductsDetailOutput, + ListProductsInput, + GetProductInput, + DeleteProductInput, + CreateProductInput, + UpdateProductInput, + CreateProductToolInput, + UpdateProductToolInput, +} from "./contracts/products"; export { ProductSchema, + ProductDetailSchema, ProductPriceSchema, ListProductsOutputSchema, + ListProductsDetailOutputSchema, + ListProductsInputSchema, + GetProductInputSchema, + DeleteProductInputSchema, + CreateProductInputSchema, + UpdateProductInputSchema, + CreateProductToolInputSchema, + UpdateProductToolInputSchema, } from "./contracts/products"; export type { RecurringInterval, @@ -60,19 +117,35 @@ export type { Customer, CustomerWithSubscriptions } from "./schemas/customer"; export { CustomerSchema, CustomerWithSubscriptionsSchema, - GetCustomerInputSchema, + GetCustomerInputSchema as SdkGetCustomerInputSchema, } from "./schemas/customer"; // New MCP schemas export type { Order, OrderItem, OrderStatus } from "./schemas/order"; +export type { + OrderWithRelations, + ListOrdersOutput, + ListOrdersPaginatedInput, + ListOrdersPaginatedOutput, + GetOrderInput, +} from "./contracts/order"; +export { + OrderWithRelationsSchema, + ListOrdersOutputSchema, + ListOrdersPaginatedInputSchema, + ListOrdersPaginatedOutputSchema, + GetOrderInputSchema, +} from "./contracts/order"; export { OrderSchema, OrderItemSchema, OrderStatusSchema, } from "./schemas/order"; -export type { PaginationInput, PaginationOutput } from "./schemas/pagination"; +export type { IdInput, PaginationInput, PaginatedInput, PaginationOutput } from "./schemas/pagination"; export { + IdInputSchema, PaginationInputSchema, + PaginatedInputSchema, PaginationOutputSchema, } from "./schemas/pagination"; export type { @@ -114,18 +187,27 @@ export const sdkContract = { // MCP contract - only the methods the MCP router implements export const mcpContract = { customer: { - list: customer.list, + list: customer.listPaginated, get: customer.get, create: customer.create, update: customer.update, delete: customer.delete, }, - order, + order: { + list: order.listPaginated, + get: order.get, + }, checkout: { list: checkout.listSummary, get: checkout.getSummary, }, - products, + products: { + list: products.listPaginated, + get: products.get, + create: products.create, + update: products.update, + delete: products.delete, + }, }; export type { MetadataValidationError } from "./validation/metadata-validation"; @@ -135,3 +217,6 @@ export { MAX_METADATA_SIZE_BYTES, validateMetadata, } from "./validation/metadata-validation"; + +export type { Result } from "./lib/utils"; +export { ok, err } from "./lib/utils"; diff --git a/src/schemas/checkout.ts b/src/schemas/checkout.ts index 1454e85..474805e 100644 --- a/src/schemas/checkout.ts +++ b/src/schemas/checkout.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { CurrencySchema } from "./currency"; +import { CustomerSchema } from "./customer"; import { BaseInvoiceSchema, DynamicAmountPendingInvoiceSchema, @@ -189,3 +190,43 @@ export const CheckoutSchema = z.union([ ]); export type Checkout = z.infer; + +// Simple enum schemas for filtering/display +export const CheckoutStatusSchema = z.enum([ + "UNCONFIRMED", + "CONFIRMED", + "PENDING_PAYMENT", + "PAYMENT_RECEIVED", + "EXPIRED", +]); +export type CheckoutStatus = z.infer; + +export const CheckoutTypeSchema = z.enum(["PRODUCTS", "AMOUNT", "TOP_UP"]); +export type CheckoutType = z.infer; + +// Summary schema for list views (lighter than full CheckoutSchema) +export const CheckoutListItemSchema = z.object({ + id: z.string(), + status: CheckoutStatusSchema, + type: CheckoutTypeSchema, + currency: CurrencySchema, + totalAmount: z.number().nullable(), + customerId: z.string().nullable(), + customer: CustomerSchema.nullable(), + productId: z.string().nullable(), + organizationId: z.string(), + expiresAt: z.date(), + createdAt: z.date(), + modifiedAt: z.date().nullable(), +}); +export type CheckoutListItem = z.infer; + +// Detail schema (includes additional fields beyond list item) +export const CheckoutDetailSchema = CheckoutListItemSchema.extend({ + userMetadata: z.record(z.unknown()).nullable(), + successUrl: z.string().nullable(), + discountAmount: z.number().nullable(), + netAmount: z.number().nullable(), + taxAmount: z.number().nullable(), +}); +export type CheckoutDetail = z.infer; diff --git a/src/schemas/order.ts b/src/schemas/order.ts index cf4cd2b..88bcf56 100644 --- a/src/schemas/order.ts +++ b/src/schemas/order.ts @@ -49,3 +49,16 @@ export const OrderSchema = z.object({ }); export type Order = z.infer; + +// Import CustomerSchema for relations (lazy to avoid circular deps) +import { CustomerSchema } from "./customer"; + +/** + * Order with related customer and items for detailed views. + */ +export const OrderWithRelationsSchema = OrderSchema.extend({ + customer: CustomerSchema.nullable(), + orderItems: z.array(OrderItemSchema), +}); + +export type OrderWithRelations = z.infer; diff --git a/src/schemas/pagination.ts b/src/schemas/pagination.ts index f9b9a8d..e8ce33e 100644 --- a/src/schemas/pagination.ts +++ b/src/schemas/pagination.ts @@ -1,5 +1,14 @@ import { z } from "zod"; +/** + * Common ID input for get/delete operations. + */ +export const IdInputSchema = z.object({ + id: z.string(), +}); + +export type IdInput = z.infer; + /** * Pagination input schema for list operations. * Uses cursor-based pagination for efficient large dataset traversal. @@ -11,6 +20,17 @@ export const PaginationInputSchema = z.object({ export type PaginationInput = z.infer; +/** + * Pagination input with descriptions (for AI tools). + * Use .extend() to add entity-specific filters. + */ +export const PaginatedInputSchema = z.object({ + limit: z.number().optional().describe("Maximum number of items to return (1-100, default 50)"), + cursor: z.string().optional().describe("Cursor for pagination (from previous response)"), +}); + +export type PaginatedInput = z.infer; + /** * Pagination output schema for list operations. * Returns a cursor for the next page, or null if no more results. diff --git a/src/schemas/product.ts b/src/schemas/product.ts index c64c201..0a3b27a 100644 --- a/src/schemas/product.ts +++ b/src/schemas/product.ts @@ -1,20 +1,35 @@ import { z } from "zod"; import { CurrencySchema } from "./currency"; +import { RecurringIntervalSchema } from "./subscription"; -export const CheckoutProductPriceSchema = z.object({ +// Price schema - used in product responses +export const ProductPriceSchema = z.object({ id: z.string(), amountType: z.enum(["FIXED", "CUSTOM"]), priceAmount: z.number().nullable(), currency: CurrencySchema, }); +export type ProductPrice = z.infer; -// Checkout products have a prices array to allow future support of metered pricing -// (e.g., base subscription + usage-based charges). Currently only one static price -// (FIXED/CUSTOM) is supported. -export const CheckoutProductSchema = z.object({ +// Core product fields +export const ProductSchema = z.object({ id: z.string(), name: z.string(), description: z.string().nullable(), - recurringInterval: z.enum(["MONTH", "QUARTER", "YEAR"]).nullable(), - prices: z.array(CheckoutProductPriceSchema), + recurringInterval: RecurringIntervalSchema.nullable(), + prices: z.array(ProductPriceSchema), }); +export type Product = z.infer; + +// Extended with administrative metadata +export const ProductDetailSchema = ProductSchema.extend({ + userMetadata: z.record(z.string(), z.unknown()).nullable(), + organizationId: z.string(), + createdAt: z.date(), + modifiedAt: z.date().nullable(), +}); +export type ProductDetail = z.infer; + +// Aliases for checkout context (backwards compat) +export const CheckoutProductPriceSchema = ProductPriceSchema; +export const CheckoutProductSchema = ProductSchema; From 73d04311c66da5bbda75565d53ee0dd34e339edd Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 29 Jan 2026 15:05:40 -0300 Subject: [PATCH 2/7] fix: biome formatting --- src/contracts/checkout.ts | 43 ++++++++++++++++--------- src/contracts/customer.ts | 43 ++++++++++++++++++------- src/contracts/order.ts | 20 +++++++++--- src/contracts/products.ts | 67 ++++++++++++++++++++++++--------------- src/index.ts | 7 +++- src/schemas/pagination.ts | 10 ++++-- 6 files changed, 130 insertions(+), 60 deletions(-) diff --git a/src/contracts/checkout.ts b/src/contracts/checkout.ts index 7ff7c3d..c1c2804 100644 --- a/src/contracts/checkout.ts +++ b/src/contracts/checkout.ts @@ -1,18 +1,21 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; import { - CheckoutSchema, - CheckoutStatusSchema, - CheckoutTypeSchema, - CheckoutListItemSchema, + type CheckoutDetail, CheckoutDetailSchema, + type CheckoutListItem, + CheckoutListItemSchema, + CheckoutSchema, type CheckoutStatus, + CheckoutStatusSchema, type CheckoutType, - type CheckoutListItem, - type CheckoutDetail, + CheckoutTypeSchema, } from "../schemas/checkout"; import { CurrencySchema } from "../schemas/currency"; -import { PaginatedInputSchema, PaginationOutputSchema } from "../schemas/pagination"; +import { + PaginatedInputSchema, + PaginationOutputSchema, +} from "../schemas/pagination"; // Re-export entity schemas for backwards compatibility export { @@ -122,19 +125,29 @@ export const ListCheckoutsOutputSchema = z.object({ export type ListCheckoutsOutput = z.infer; export const ListCheckoutsPaginatedInputSchema = PaginatedInputSchema.extend({ - status: CheckoutStatusSchema.optional().describe("Filter by status: UNCONFIRMED, CONFIRMED, PENDING_PAYMENT, PAYMENT_RECEIVED, or EXPIRED"), -}); -export type ListCheckoutsPaginatedInput = z.infer; - -export const ListCheckoutsPaginatedOutputSchema = PaginationOutputSchema.extend({ - checkouts: z.array(CheckoutSchema), + status: CheckoutStatusSchema.optional().describe( + "Filter by status: UNCONFIRMED, CONFIRMED, PENDING_PAYMENT, PAYMENT_RECEIVED, or EXPIRED", + ), }); -export type ListCheckoutsPaginatedOutput = z.infer; +export type ListCheckoutsPaginatedInput = z.infer< + typeof ListCheckoutsPaginatedInputSchema +>; + +export const ListCheckoutsPaginatedOutputSchema = PaginationOutputSchema.extend( + { + checkouts: z.array(CheckoutSchema), + }, +); +export type ListCheckoutsPaginatedOutput = z.infer< + typeof ListCheckoutsPaginatedOutputSchema +>; export const ListCheckoutsSummaryOutputSchema = PaginationOutputSchema.extend({ checkouts: z.array(CheckoutListItemSchema), }); -export type ListCheckoutsSummaryOutput = z.infer; +export type ListCheckoutsSummaryOutput = z.infer< + typeof ListCheckoutsSummaryOutputSchema +>; // Contracts export const createCheckoutContract = oc diff --git a/src/contracts/customer.ts b/src/contracts/customer.ts index c5b5f18..06b3234 100644 --- a/src/contracts/customer.ts +++ b/src/contracts/customer.ts @@ -5,7 +5,10 @@ import { CustomerWithSubscriptionsSchema, GetCustomerInputSchema as SdkGetCustomerInputSchema, } from "../schemas/customer"; -import { PaginatedInputSchema, PaginationOutputSchema } from "../schemas/pagination"; +import { + PaginatedInputSchema, + PaginationOutputSchema, +} from "../schemas/pagination"; // Simple list (no pagination) export const ListCustomersOutputSchema = z.object({ @@ -15,19 +18,28 @@ export type ListCustomersOutput = z.infer; // Paginated list (no additional filters for customers) export const ListCustomersPaginatedInputSchema = PaginatedInputSchema; -export type ListCustomersPaginatedInput = z.infer; - -export const ListCustomersPaginatedOutputSchema = PaginationOutputSchema.extend({ - customers: z.array(CustomerSchema), -}); -export type ListCustomersPaginatedOutput = z.infer; +export type ListCustomersPaginatedInput = z.infer< + typeof ListCustomersPaginatedInputSchema +>; + +export const ListCustomersPaginatedOutputSchema = PaginationOutputSchema.extend( + { + customers: z.array(CustomerSchema), + }, +); +export type ListCustomersPaginatedOutput = z.infer< + typeof ListCustomersPaginatedOutputSchema +>; // Flexible customer lookup - exactly one of id, email, or externalId // Base shape without refinement (for MCP tool schemas) export const CustomerLookupBaseSchema = z.object({ id: z.string().optional().describe("The customer ID"), email: z.string().optional().describe("The customer email address"), - externalId: z.string().optional().describe("The external ID from your system"), + externalId: z + .string() + .optional() + .describe("The external ID from your system"), }); // With refinement for runtime validation @@ -47,15 +59,24 @@ export type DeleteCustomerInput = z.infer; export const CreateCustomerInputSchema = z.object({ name: z.string().min(1).describe("Customer name"), email: z.string().email().describe("Customer email address"), - externalId: z.string().optional().describe("External ID from your system for linking"), + externalId: z + .string() + .optional() + .describe("External ID from your system for linking"), }); export const UpdateCustomerInputSchema = z.object({ id: z.string().describe("The customer ID to update"), name: z.string().optional().describe("New customer name"), email: z.string().email().optional().describe("New customer email address"), - externalId: z.string().optional().describe("External ID from your system for linking"), - userMetadata: z.record(z.string(), z.string()).optional().describe("Custom metadata key-value pairs"), + externalId: z + .string() + .optional() + .describe("External ID from your system for linking"), + userMetadata: z + .record(z.string(), z.string()) + .optional() + .describe("Custom metadata key-value pairs"), }); export type CreateCustomerInput = z.infer; diff --git a/src/contracts/order.ts b/src/contracts/order.ts index 3503834..b88730c 100644 --- a/src/contracts/order.ts +++ b/src/contracts/order.ts @@ -1,10 +1,13 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; import { - OrderWithRelationsSchema, type OrderWithRelations, + OrderWithRelationsSchema, } from "../schemas/order"; -import { PaginatedInputSchema, PaginationOutputSchema } from "../schemas/pagination"; +import { + PaginatedInputSchema, + PaginationOutputSchema, +} from "../schemas/pagination"; // Re-export entity schema for backwards compatibility export { OrderWithRelationsSchema }; @@ -18,14 +21,21 @@ export type ListOrdersOutput = z.infer; export const ListOrdersPaginatedInputSchema = PaginatedInputSchema.extend({ customerId: z.string().optional().describe("Filter by customer ID"), - status: z.string().optional().describe("Filter by status: PENDING, PAID, REFUNDED, or CANCELLED"), + status: z + .string() + .optional() + .describe("Filter by status: PENDING, PAID, REFUNDED, or CANCELLED"), }); -export type ListOrdersPaginatedInput = z.infer; +export type ListOrdersPaginatedInput = z.infer< + typeof ListOrdersPaginatedInputSchema +>; export const ListOrdersPaginatedOutputSchema = PaginationOutputSchema.extend({ orders: z.array(OrderWithRelationsSchema), }); -export type ListOrdersPaginatedOutput = z.infer; +export type ListOrdersPaginatedOutput = z.infer< + typeof ListOrdersPaginatedOutputSchema +>; export const GetOrderInputSchema = z.object({ id: z.string().describe("The order ID"), diff --git a/src/contracts/products.ts b/src/contracts/products.ts index dc7c7d8..6e98d48 100644 --- a/src/contracts/products.ts +++ b/src/contracts/products.ts @@ -1,27 +1,26 @@ import { oc } from "@orpc/contract"; import { z } from "zod"; import { CurrencySchema } from "../schemas/currency"; -import { PaginatedInputSchema, PaginationOutputSchema } from "../schemas/pagination"; import { - PriceAmountTypeSchema, - ProductPriceInputSchema, - RecurringIntervalInputSchema, -} from "../schemas/product-price-input"; + PaginatedInputSchema, + PaginationOutputSchema, +} from "../schemas/pagination"; import { - ProductSchema, - ProductDetailSchema, - ProductPriceSchema, type Product, type ProductDetail, + ProductDetailSchema, type ProductPrice, + ProductPriceSchema, + ProductSchema, } from "../schemas/product"; +import { + PriceAmountTypeSchema, + ProductPriceInputSchema, + RecurringIntervalInputSchema, +} from "../schemas/product-price-input"; // Re-export entity schemas for backwards compatibility -export { - ProductSchema, - ProductDetailSchema, - ProductPriceSchema, -}; +export { ProductSchema, ProductDetailSchema, ProductPriceSchema }; export type { Product, ProductDetail, ProductPrice }; // List output schemas @@ -33,7 +32,9 @@ export type ListProductsOutput = z.infer; export const ListProductsDetailOutputSchema = PaginationOutputSchema.extend({ products: z.array(ProductDetailSchema), }); -export type ListProductsDetailOutput = z.infer; +export type ListProductsDetailOutput = z.infer< + typeof ListProductsDetailOutputSchema +>; // Simple list without pagination export const listProductsContract = oc @@ -71,13 +72,20 @@ export type UpdateProductInput = z.infer; export const CreateProductToolInputSchema = z.object({ name: z.string().min(1).describe("Product name"), description: z.string().optional().describe("Product description"), - priceAmount: z.number().optional().describe( - "Price amount (in cents for USD, whole sats for SAT). Required for fixed pricing." + priceAmount: z + .number() + .optional() + .describe( + "Price amount (in cents for USD, whole sats for SAT). Required for fixed pricing.", + ), + currency: CurrencySchema.optional().describe( + "Currency: USD or SAT (default: USD)", + ), + amountType: PriceAmountTypeSchema.optional().describe( + "Amount type: FIXED or CUSTOM (default: FIXED)", ), - currency: CurrencySchema.optional().describe("Currency: USD or SAT (default: USD)"), - amountType: PriceAmountTypeSchema.optional().describe("Amount type: FIXED or CUSTOM (default: FIXED)"), recurringInterval: RecurringIntervalInputSchema.optional().describe( - "Recurring interval: NEVER (one-time), MONTH, QUARTER, or YEAR (default: NEVER)" + "Recurring interval: NEVER (one-time), MONTH, QUARTER, or YEAR (default: NEVER)", ), }); @@ -85,18 +93,25 @@ export const UpdateProductToolInputSchema = z.object({ id: z.string().describe("The product ID to update"), name: z.string().optional().describe("New product name"), description: z.string().optional().describe("New product description"), - priceAmount: z.number().optional().describe( - "New price amount (in cents for USD, whole sats for SAT)" - ), + priceAmount: z + .number() + .optional() + .describe("New price amount (in cents for USD, whole sats for SAT)"), currency: CurrencySchema.optional().describe("Currency: USD or SAT"), - amountType: PriceAmountTypeSchema.optional().describe("Amount type: FIXED or CUSTOM"), + amountType: PriceAmountTypeSchema.optional().describe( + "Amount type: FIXED or CUSTOM", + ), recurringInterval: RecurringIntervalInputSchema.optional().describe( - "Recurring interval: NEVER, MONTH, QUARTER, or YEAR" + "Recurring interval: NEVER, MONTH, QUARTER, or YEAR", ), }); -export type CreateProductToolInput = z.infer; -export type UpdateProductToolInput = z.infer; +export type CreateProductToolInput = z.infer< + typeof CreateProductToolInputSchema +>; +export type UpdateProductToolInput = z.infer< + typeof UpdateProductToolInputSchema +>; export const GetProductInputSchema = z.object({ id: z.string().describe("The product ID"), diff --git a/src/index.ts b/src/index.ts index d4a6ffa..9d3e990 100644 --- a/src/index.ts +++ b/src/index.ts @@ -141,7 +141,12 @@ export { OrderItemSchema, OrderStatusSchema, } from "./schemas/order"; -export type { IdInput, PaginationInput, PaginatedInput, PaginationOutput } from "./schemas/pagination"; +export type { + IdInput, + PaginationInput, + PaginatedInput, + PaginationOutput, +} from "./schemas/pagination"; export { IdInputSchema, PaginationInputSchema, diff --git a/src/schemas/pagination.ts b/src/schemas/pagination.ts index e8ce33e..c266363 100644 --- a/src/schemas/pagination.ts +++ b/src/schemas/pagination.ts @@ -25,8 +25,14 @@ export type PaginationInput = z.infer; * Use .extend() to add entity-specific filters. */ export const PaginatedInputSchema = z.object({ - limit: z.number().optional().describe("Maximum number of items to return (1-100, default 50)"), - cursor: z.string().optional().describe("Cursor for pagination (from previous response)"), + limit: z + .number() + .optional() + .describe("Maximum number of items to return (1-100, default 50)"), + cursor: z + .string() + .optional() + .describe("Cursor for pagination (from previous response)"), }); export type PaginatedInput = z.infer; From 65406b9eb6121df40a0bd9bfdd98f3c8403f7de4 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 29 Jan 2026 15:18:59 -0300 Subject: [PATCH 3/7] refactor(customer): use discriminated union for lookup schema Replace refinement-based validation with a discriminated union for CustomerLookupInputSchema. This ensures exactly-one-of validation translates properly to JSON Schema for MCP/AI tool compatibility. --- src/contracts/customer.ts | 33 ++++++++++++++++----------------- src/index.ts | 1 - 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/contracts/customer.ts b/src/contracts/customer.ts index 06b3234..9eb87bd 100644 --- a/src/contracts/customer.ts +++ b/src/contracts/customer.ts @@ -31,29 +31,28 @@ export type ListCustomersPaginatedOutput = z.infer< typeof ListCustomersPaginatedOutputSchema >; -// Flexible customer lookup - exactly one of id, email, or externalId -// Base shape without refinement (for MCP tool schemas) -export const CustomerLookupBaseSchema = z.object({ - id: z.string().optional().describe("The customer ID"), - email: z.string().optional().describe("The customer email address"), - externalId: z - .string() - .optional() - .describe("The external ID from your system"), +// Customer lookup by exactly one identifier (discriminated union for JSON Schema compatibility) +const CustomerLookupByIdSchema = z.object({ + id: z.string().describe("The customer ID"), +}); +const CustomerLookupByEmailSchema = z.object({ + email: z.string().describe("The customer email address"), +}); +const CustomerLookupByExternalIdSchema = z.object({ + externalId: z.string().describe("The external ID from your system"), }); -// With refinement for runtime validation -export const CustomerLookupInputSchema = CustomerLookupBaseSchema.refine( - (data) => [data.id, data.email, data.externalId].filter(Boolean).length === 1, - { message: "Exactly one of id, email, or externalId must be provided" }, -); +export const CustomerLookupInputSchema = z.union([ + CustomerLookupByIdSchema, + CustomerLookupByEmailSchema, + CustomerLookupByExternalIdSchema, +]); export type CustomerLookupInput = z.infer; -// Aliases for specific operations -export const GetCustomerInputSchema = CustomerLookupBaseSchema; +export const GetCustomerInputSchema = CustomerLookupInputSchema; export type GetCustomerInput = z.infer; -export const DeleteCustomerInputSchema = CustomerLookupBaseSchema; +export const DeleteCustomerInputSchema = CustomerLookupInputSchema; export type DeleteCustomerInput = z.infer; export const CreateCustomerInputSchema = z.object({ diff --git a/src/index.ts b/src/index.ts index 9d3e990..24e0fa2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,7 +65,6 @@ export { GetCustomerInputSchema, DeleteCustomerInputSchema, CustomerLookupInputSchema, - CustomerLookupBaseSchema, } from "./contracts/customer"; export type { Checkout } from "./schemas/checkout"; export { CheckoutSchema } from "./schemas/checkout"; From 5c433b1182afed39f9ae0b60e07afe659cbb6201 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 29 Jan 2026 15:24:32 -0300 Subject: [PATCH 4/7] feat(customer): add CustomerLookupToolSchema for MCP tools xmcp requires .shape property for tool schema generation, which only ZodObject has. ZodUnion (CustomerLookupInputSchema) lacks .shape. Add CustomerLookupToolSchema as a flat object with all optional fields for MCP tools, while keeping the discriminated union for contract validation. --- src/contracts/customer.ts | 9 ++++++++- src/index.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/contracts/customer.ts b/src/contracts/customer.ts index 9eb87bd..ac204a6 100644 --- a/src/contracts/customer.ts +++ b/src/contracts/customer.ts @@ -31,7 +31,7 @@ export type ListCustomersPaginatedOutput = z.infer< typeof ListCustomersPaginatedOutputSchema >; -// Customer lookup by exactly one identifier (discriminated union for JSON Schema compatibility) +// Customer lookup by exactly one identifier (discriminated union for contract validation) const CustomerLookupByIdSchema = z.object({ id: z.string().describe("The customer ID"), }); @@ -49,6 +49,13 @@ export const CustomerLookupInputSchema = z.union([ ]); export type CustomerLookupInput = z.infer; +// Flat schema for MCP tools (xmcp needs .shape, unions don't have it) +export const CustomerLookupToolSchema = z.object({ + id: z.string().optional().describe("The customer ID"), + email: z.string().optional().describe("The customer email address"), + externalId: z.string().optional().describe("The external ID from your system"), +}); + export const GetCustomerInputSchema = CustomerLookupInputSchema; export type GetCustomerInput = z.infer; diff --git a/src/index.ts b/src/index.ts index 24e0fa2..800563e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,6 +65,7 @@ export { GetCustomerInputSchema, DeleteCustomerInputSchema, CustomerLookupInputSchema, + CustomerLookupToolSchema, } from "./contracts/customer"; export type { Checkout } from "./schemas/checkout"; export { CheckoutSchema } from "./schemas/checkout"; From 0d686c4f1cfced58acef4d08d647fae271b08abf Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 29 Jan 2026 15:25:31 -0300 Subject: [PATCH 5/7] fix(pagination): add validation constraints to PaginatedInputSchema --- src/schemas/pagination.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/schemas/pagination.ts b/src/schemas/pagination.ts index c266363..5ef724f 100644 --- a/src/schemas/pagination.ts +++ b/src/schemas/pagination.ts @@ -27,7 +27,10 @@ export type PaginationInput = z.infer; export const PaginatedInputSchema = z.object({ limit: z .number() - .optional() + .int() + .min(1) + .max(100) + .default(50) .describe("Maximum number of items to return (1-100, default 50)"), cursor: z .string() From 62de44933365cd0f646e5c5e9dd9a657834f7b4b Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 29 Jan 2026 15:26:33 -0300 Subject: [PATCH 6/7] fix: biome formatting --- src/contracts/customer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/contracts/customer.ts b/src/contracts/customer.ts index ac204a6..997372b 100644 --- a/src/contracts/customer.ts +++ b/src/contracts/customer.ts @@ -53,7 +53,10 @@ export type CustomerLookupInput = z.infer; export const CustomerLookupToolSchema = z.object({ id: z.string().optional().describe("The customer ID"), email: z.string().optional().describe("The customer email address"), - externalId: z.string().optional().describe("The external ID from your system"), + externalId: z + .string() + .optional() + .describe("The external ID from your system"), }); export const GetCustomerInputSchema = CustomerLookupInputSchema; From 30bdcba76943691c3bcbd2517fc551feb255366b Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 29 Jan 2026 16:27:56 -0300 Subject: [PATCH 7/7] Stop using discriminated union. MCP can't handle it --- src/contracts/customer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/customer.ts b/src/contracts/customer.ts index 997372b..69083d9 100644 --- a/src/contracts/customer.ts +++ b/src/contracts/customer.ts @@ -59,10 +59,10 @@ export const CustomerLookupToolSchema = z.object({ .describe("The external ID from your system"), }); -export const GetCustomerInputSchema = CustomerLookupInputSchema; +export const GetCustomerInputSchema = CustomerLookupToolSchema; export type GetCustomerInput = z.infer; -export const DeleteCustomerInputSchema = CustomerLookupInputSchema; +export const DeleteCustomerInputSchema = CustomerLookupToolSchema; export type DeleteCustomerInput = z.infer; export const CreateCustomerInputSchema = z.object({