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;
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It clearly indicates this is a write/mutation operation ('places the order') and mentions the testing capability via 'dry_run'. However, it doesn't disclose important behavioral aspects like authentication requirements, rate limits, error conditions, or what happens when promo codes are invalid. The description adds some value but leaves significant gaps for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is perfectly concise with two sentences that each earn their place. The first sentence states the purpose and key actions, while the second provides crucial behavioral context about testing. There's zero wasted text, and the most important information (the tool's purpose) is front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex mutation tool with 15 parameters and no annotations or output schema, the description is adequate but incomplete. It covers the basic purpose and testing capability well, but doesn't address important contextual aspects like authentication requirements, error handling, or what the tool returns. Given the complexity and lack of structured metadata, a more comprehensive description would be beneficial.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all 15 parameters thoroughly. The description mentions 'promo codes' and 'dry_run' specifically, but doesn't add meaningful semantic context beyond what's in the schema descriptions. The baseline of 3 is appropriate when the schema does the heavy lifting for parameter documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Complete a Sephora purchase') and enumerates the key steps involved (fills shipping address, applies promo codes, enters payment details, places order). It distinguishes itself from sibling tools like 'sephora_add_to_basket' or 'sephora_view_basket' by focusing on the final checkout process rather than earlier shopping stages.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context about when to use this tool (to complete a purchase) and includes explicit guidance about the 'dry_run' parameter for testing. However, it doesn't explicitly mention when NOT to use it or name specific alternatives among the sibling tools, though the distinction is implied by the tool's focus on checkout.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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