sephora_checkout
Complete Sephora purchases by filling shipping details, applying promo codes, and entering payment information. Use dry_run mode to test checkout flow without placing orders.
Instructions
Complete a Sephora purchase. Fills shipping address, applies optional promo codes, enters payment details, and places the order. Set dry_run=true to test the flow without actually placing an order.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| Yes | Email address for the order | ||
| first_name | Yes | First name | |
| last_name | Yes | Last name | |
| address_line1 | Yes | Street address line 1 | |
| address_line2 | No | Street address line 2 (optional) | |
| city | Yes | City | |
| state | Yes | 2-letter US state code, e.g. 'CA' | |
| zip | Yes | ZIP code (5 or 9 digit) | |
| phone | Yes | Phone number | |
| card_number | Yes | Credit/debit card number (digits only) | |
| card_expiry | Yes | Card expiry date in MM/YY or MM/YYYY format | |
| card_cvv | Yes | Card CVV/CVC code | |
| card_name | Yes | Name as it appears on card | |
| promo_code | No | Optional promotional/discount code | |
| dry_run | No | If true, fill in checkout form but do NOT submit the order (useful for testing) |
Implementation Reference
- src/tools/checkout.ts:42-380 (handler)The main handler function for sephora_checkout tool. Implements the complete checkout flow including: navigating to checkout page, checking for empty basket, filling contact/shipping address, applying promo codes, entering payment details (supporting both iframe and direct card fields), and placing the order. Supports dry_run mode for testing without submitting.
export async function checkout(input: CheckoutInput): Promise<string> { const session = getSession(); const page = await session.navigateTo("/checkout"); const errors: string[] = []; try { // Wait for checkout page await page.waitForSelector( '[data-comp="CheckoutPage"], [class*="checkout-page"], form[class*="checkout"]', { timeout: 20000 } ); } catch { await page.waitForTimeout(3000); } // Check if basket is empty const isEmptyBasket = await page.evaluate(() => { return !!document.querySelector('[class*="empty-basket"], [data-at="empty_basket"]'); }); if (isEmptyBasket) { return JSON.stringify({ success: false, dry_run: input.dry_run ?? false, stage: "basket_check", message: "Cannot checkout: basket is empty. Add items first.", }); } // --- Step 1: Fill contact information / email --- async function fillField( selectors: string[], value: string, label: string ): Promise<boolean> { for (const selector of selectors) { try { const el = page.locator(selector).first(); if (await el.isVisible({ timeout: 2000 })) { await el.fill(value); return true; } } catch { continue; } } errors.push(`Could not find field: ${label}`); return false; } async function clickButton(selectors: string[], label: string): Promise<boolean> { for (const selector of selectors) { try { const btn = page.locator(selector).first(); if (await btn.isVisible({ timeout: 2000 })) { await btn.click(); await page.waitForTimeout(1000); return true; } } catch { continue; } } errors.push(`Could not find button: ${label}`); return false; } // Fill email (guest checkout or sign-in) const guestButton = page.locator( 'button:has-text("Guest"), button:has-text("Continue as Guest"), [data-at="guest_checkout"]' ).first(); if (await guestButton.isVisible({ timeout: 3000 }).catch(() => false)) { await guestButton.click(); await page.waitForTimeout(1000); } await fillField( ['input[type="email"]', 'input[name="email"]', '[data-at="email_input"]'], input.email, "email" ); // --- Step 2: Shipping Address --- await fillField( ['input[name="firstName"]', '[data-at="first_name"]', 'input[placeholder*="First" i]'], input.first_name, "first name" ); await fillField( ['input[name="lastName"]', '[data-at="last_name"]', 'input[placeholder*="Last" i]'], input.last_name, "last name" ); await fillField( ['input[name="address1"]', '[data-at="address_line1"]', 'input[placeholder*="Address" i]'], input.address_line1, "address line 1" ); if (input.address_line2) { await fillField( ['input[name="address2"]', '[data-at="address_line2"]', 'input[placeholder*="Apt" i]'], input.address_line2, "address line 2" ); } await fillField( ['input[name="city"]', '[data-at="city"]', 'input[placeholder*="City" i]'], input.city, "city" ); // State dropdown try { const stateSelect = page.locator('select[name="state"], select[data-at="state"]').first(); if (await stateSelect.isVisible({ timeout: 2000 })) { await stateSelect.selectOption(input.state); } } catch { await fillField( ['input[name="state"]', '[data-at="state"]'], input.state, "state" ); } await fillField( ['input[name="zip"]', 'input[name="postalCode"]', '[data-at="zip"]', 'input[placeholder*="ZIP" i]'], input.zip, "ZIP code" ); await fillField( ['input[name="phone"]', 'input[type="tel"]', '[data-at="phone"]'], input.phone, "phone" ); // Apply promo code if provided if (input.promo_code) { try { const promoInput = page.locator( 'input[name="promoCode"], [data-at="promo_code_input"]' ).first(); if (await promoInput.isVisible({ timeout: 2000 })) { await promoInput.fill(input.promo_code); await clickButton( ['button[data-at="apply_promo"]', 'button:has-text("Apply")'], "Apply promo" ); } } catch { errors.push("Could not apply promo code"); } } // Continue to payment await clickButton( [ '[data-at="continue_to_payment"]', 'button:has-text("Continue to Payment")', 'button:has-text("Continue")', 'button[type="submit"]', ], "Continue to Payment" ); await page.waitForTimeout(2000); // --- Step 3: Payment --- // Handle iframe-embedded card fields (common for Sephora) const cardFrameSelectors = [ 'iframe[name*="card"]', 'iframe[id*="card"]', 'iframe[src*="payment"]', ]; let cardFrame = null; for (const sel of cardFrameSelectors) { try { const frame = page.frameLocator(sel).first(); // Test if frame exists await frame.locator("input").first().waitFor({ timeout: 2000 }); cardFrame = frame; break; } catch { continue; } } if (cardFrame) { // Fill card fields within iframe try { await cardFrame.locator('input[name*="cardNumber"], input[placeholder*="Card number" i]').first().fill(input.card_number); await cardFrame.locator('input[name*="expiry"], input[name*="exp"]').first().fill(input.card_expiry); await cardFrame.locator('input[name*="cvv"], input[name*="cvc"], input[name*="securityCode"]').first().fill(input.card_cvv); } catch { errors.push("Could not fill card iframe fields"); } } else { // Direct card fields await fillField( [ 'input[name="cardNumber"]', 'input[data-at="card_number"]', 'input[placeholder*="Card Number" i]', 'input[autocomplete="cc-number"]', ], input.card_number, "card number" ); await fillField( [ 'input[name="expDate"]', 'input[name="expiry"]', 'input[placeholder*="MM/YY" i]', 'input[autocomplete="cc-exp"]', ], input.card_expiry, "expiry date" ); await fillField( [ 'input[name="cvv"]', 'input[name="securityCode"]', 'input[placeholder*="CVV" i]', 'input[autocomplete="cc-csc"]', ], input.card_cvv, "CVV" ); await fillField( [ 'input[name="nameOnCard"]', 'input[placeholder*="Name on card" i]', 'input[autocomplete="cc-name"]', ], input.card_name, "name on card" ); } if (input.dry_run) { // Capture order summary without submitting const orderSummary = await page.evaluate(() => { const totalEl = document.querySelector('[data-at="order_total"]') ?? document.querySelector('[class*="order-total"]'); return { total: totalEl?.textContent?.trim() ?? "Unknown", }; }); return JSON.stringify({ success: true, dry_run: true, stage: "payment_form_filled", order_total: orderSummary.total, message: "Dry run complete: checkout form filled successfully. Order was NOT submitted (dry_run=true).", errors: errors.length > 0 ? errors : undefined, } satisfies CheckoutResult); } // --- Step 4: Place Order --- const submitClicked = await clickButton( [ '[data-at="place_order_button"]', 'button:has-text("Place Order")', 'button:has-text("Submit Order")', 'button[type="submit"][class*="place"]', ], "Place Order" ); if (!submitClicked) { return JSON.stringify({ success: false, dry_run: false, stage: "order_submission", message: "Could not find the Place Order button.", errors, } satisfies CheckoutResult); } // Wait for order confirmation try { await page.waitForSelector( '[data-at="order_confirmation"], [class*="order-confirmation"], [class*="confirmation-page"]', { timeout: 30000 } ); } catch { await page.waitForTimeout(5000); } // Extract order confirmation details const confirmation = await page.evaluate(() => { const orderNumEl = document.querySelector('[data-at="order_number"]') ?? document.querySelector('[class*="order-number"]'); const totalEl = document.querySelector('[data-at="order_total"]') ?? document.querySelector('[class*="order-total"]'); const deliveryEl = document.querySelector('[data-at="estimated_delivery"]') ?? document.querySelector('[class*="delivery-date"]'); const orderText = orderNumEl?.textContent?.trim() ?? ""; const orderMatch = orderText.match(/[A-Z0-9-]{6,}/); return { order_number: orderMatch?.[0] ?? orderText, total: totalEl?.textContent?.trim() ?? "Unknown", delivery: deliveryEl?.textContent?.trim(), }; }); // Clear basket count in session session.updateState({ basketItemCount: 0 }); return JSON.stringify({ success: true, dry_run: false, stage: "order_placed", order_number: confirmation.order_number || undefined, order_total: confirmation.total, estimated_delivery: confirmation.delivery, message: confirmation.order_number ? `Order placed successfully! Order #${confirmation.order_number}` : "Order placed successfully!", errors: errors.length > 0 ? errors : undefined, } satisfies CheckoutResult); } - src/tools/checkout.ts:4-27 (schema)Zod schema defining input validation for sephora_checkout tool. Validates email, shipping address fields (name, address, city, state, zip, phone), payment card details (number, expiry, CVV, name on card), optional promo code, and dry_run flag.
export const checkoutSchema = z.object({ email: z.string().email().describe("Email address for the order"), first_name: z.string().min(1).describe("First name"), last_name: z.string().min(1).describe("Last name"), address_line1: z.string().min(1).describe("Street address line 1"), address_line2: z.string().optional().describe("Street address line 2 (optional)"), city: z.string().min(1).describe("City"), state: z.string().length(2).describe("2-letter US state code, e.g. 'CA'"), zip: z.string().regex(/^\d{5}(-\d{4})?$/).describe("ZIP code (5 or 9 digit)"), phone: z.string().regex(/^\+?[\d\s\-().]{10,}$/).describe("Phone number"), card_number: z.string().regex(/^\d{13,19}$/).describe("Credit/debit card number (digits only)"), card_expiry: z .string() .regex(/^(0[1-9]|1[0-2])\/\d{2,4}$/) .describe("Card expiry date in MM/YY or MM/YYYY format"), card_cvv: z.string().regex(/^\d{3,4}$/).describe("Card CVV/CVC code"), card_name: z.string().min(1).describe("Name as it appears on card"), promo_code: z.string().optional().describe("Optional promotional/discount code"), dry_run: z .boolean() .optional() .default(false) .describe("If true, fill in checkout form but do NOT submit the order (useful for testing)"), }); - src/index.ts:126-131 (registration)Tool definition in TOOLS array that registers sephora_checkout with MCP. Defines the tool name, description, and links to the inputSchema via zodToJsonSchema conversion.
{ name: "sephora_checkout", description: "Complete a Sephora purchase. Fills shipping address, applies optional promo codes, enters payment details, and places the order. Set dry_run=true to test the flow without actually placing an order.", inputSchema: zodToJsonSchema(checkoutSchema) as Tool["inputSchema"], }, - src/index.ts:162-165 (registration)Handler registration in toolHandlers object. Parses input against checkoutSchema and delegates to the checkout function.
sephora_checkout: async (args) => { const input = checkoutSchema.parse(args); return checkout(input); }, - src/session.ts:161-166 (helper)getSession singleton function that provides the shared SephoraSession instance used by the checkout handler to navigate to pages and interact with the browser automation.
export function getSession(): SephoraSession { if (!sessionInstance) { sessionInstance = new SephoraSession(); } return sessionInstance; }