Skip to main content
Glama

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
NameRequiredDescriptionDefault
emailYesEmail address for the order
first_nameYesFirst name
last_nameYesLast name
address_line1YesStreet address line 1
address_line2NoStreet address line 2 (optional)
cityYesCity
stateYes2-letter US state code, e.g. 'CA'
zipYesZIP code (5 or 9 digit)
phoneYesPhone number
card_numberYesCredit/debit card number (digits only)
card_expiryYesCard expiry date in MM/YY or MM/YYYY format
card_cvvYesCard CVV/CVC code
card_nameYesName as it appears on card
promo_codeNoOptional promotional/discount code
dry_runNoIf true, fill in checkout form but do NOT submit the order (useful for testing)

Implementation Reference

  • 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);
    }
  • 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);
    },
  • 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;
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/markswendsen-code/mcp-sephora'

If you have feedback or need assistance with the MCP directory API, please join our Discord server