Skip to main content
Glama

Amazon Order History CSV Download MCP

by marcusquinn
adapter.ts13.3 kB
/** * Amazon platform adapter implementing IPlatformPlugin. */ import { Page } from "playwright"; import { BasePlatformPlugin, AuthStatus, OrderListParams, OrderHeader, OrderDetails, Region, } from "../core/types"; import { Item } from "../core/types/item"; import { Shipment } from "../core/types/shipment"; import { Transaction } from "../core/types/transaction"; import { parseMoney } from "../core/types/money"; import { parseDate } from "../core/utils/date"; import { getTextByXPaths, getElementsByXPath } from "../core/utils/extraction"; import { AMAZON_REGIONS, getRegionByCode } from "./regions"; import { extractItems } from "./extractors/items"; import { extractShipments } from "./extractors/shipments"; import { extractTransactions } from "./extractors/transactions"; /** * Amazon platform plugin. */ export class AmazonPlugin extends BasePlatformPlugin { readonly name = "Amazon"; readonly slug = "amazon"; readonly supportedRegions: Region[] = AMAZON_REGIONS; /** * Check if user is authenticated on Amazon. */ async checkAuthStatus(page: Page, region: string): Promise<AuthStatus> { const regionConfig = getRegionByCode(region); if (!regionConfig) { return { authenticated: false, region, message: `Unknown region: ${region}`, }; } try { // Check current URL - if already on Amazon, don't navigate again const currentUrl = page.url(); if (!currentUrl.includes(regionConfig.domain)) { const url = `https://www.${regionConfig.domain}/gp/css/order-history`; await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60000 }); // Wait for page content instead of fixed delay await page .waitForSelector( ".order-card, #signInSubmit, #ap_email, .a-box-group", { timeout: 2000 }, ) .catch(() => {}); } // Check for sign-in page indicators (multiple patterns) const signInIndicators = [ "#signInSubmit", "#ap_email", 'input[name="email"]', "#ap_password", '[data-action="sign-in"]', ".auth-pagelet-container", ]; for (const selector of signInIndicators) { const count = await page.locator(selector).count(); if (count > 0) { return { authenticated: false, region, message: "Not logged in - sign in required", }; } } // If we're on any Amazon page that's NOT a sign-in page, we're probably logged in // Check for common logged-in indicators const loggedInIndicators = [ "#nav-link-accountList", "#nav-orders", ".nav-line-1", "#nav-al-container", ]; for (const selector of loggedInIndicators) { const count = await page.locator(selector).count(); if (count > 0) { // Try to get username const username = await getTextByXPaths( page, [ '//span[@id="nav-link-accountList-nav-line-1"]', '//span[contains(@class, "nav-line-1")]', ], "", ); return { authenticated: true, username: username || undefined, region, message: "Authenticated", }; } } // Check if page contains order-related content const pageContent = await page.content(); if ( pageContent.includes("your-orders") || pageContent.includes("order-card") || pageContent.includes("orderCard") ) { return { authenticated: true, region, message: "Authenticated (detected order content)", }; } return { authenticated: false, region, message: "Unable to determine authentication status", }; } catch (error) { return { authenticated: false, region, message: `Error checking auth: ${error}`, }; } } /** * Get login URL for a region. */ getLoginUrl(region: string): string { const regionConfig = getRegionByCode(region); const domain = regionConfig?.domain || "amazon.com"; return `https://www.${domain}/ap/signin`; } /** * Get order list URL with filters. */ getOrderListUrl(region: string, params: OrderListParams): string { const regionConfig = getRegionByCode(region); const domain = regionConfig?.domain || "amazon.com"; const language = regionConfig?.language || "en_US"; const baseUrl = `https://www.${domain}/your-orders/orders`; const queryParams = new URLSearchParams(); if (params.year) { queryParams.set("timeFilter", `year-${params.year}`); } else if (params.months) { queryParams.set("timeFilter", `months-${params.months}`); } if (params.startIndex) { queryParams.set("startIndex", params.startIndex.toString()); } queryParams.set("language", language); return `${baseUrl}?${queryParams.toString()}`; } /** * Get order detail URL. */ getOrderDetailUrl(orderId: string, region: string): string { const regionConfig = getRegionByCode(region); const domain = regionConfig?.domain || "amazon.com"; return `https://www.${domain}/gp/your-account/order-details?orderID=${orderId}`; } /** * Extract order headers from order list page. */ async extractOrderHeaders( page: Page, region: string, ): Promise<OrderHeader[]> { const headers: OrderHeader[] = []; const regionConfig = getRegionByCode(region); const currency = regionConfig?.currency || "USD"; // Find order cards using multiple strategies const orderCards = await getElementsByXPath( page, '//*[contains(@class, "js-order-card") or @id="orderCard"]', ); for (const card of orderCards) { try { // Extract order ID const orderId = await card .locator('[data-a-popover*="orderId"]') .first() .getAttribute("data-a-popover") .catch(() => null); let id = ""; if (orderId) { const match = orderId.match(/orderId['":\s]+([0-9-]+)/); id = match ? match[1] : ""; } if (!id) { // Try alternate extraction const orderIdText = await card .locator('.yohtmlc-order-id span, [data-test-id="order-id"]') .first() .textContent() .catch(() => ""); id = orderIdText?.replace(/[^0-9-]/g, "") || ""; } if (!id) continue; // Extract date const dateText = await card .locator('.order-info .value, [data-test-id="order-date"]') .first() .textContent() .catch(() => ""); const date = parseDate(dateText || ""); // Extract total const totalText = await card .locator('.yohtmlc-order-total .value, [data-test-id="order-total"]') .first() .textContent() .catch(() => ""); const total = parseMoney(totalText || "", currency); // Extract detail URL const detailLink = await card .locator('a[href*="order-details"]') .first() .getAttribute("href") .catch(() => ""); const detailUrl = detailLink ? `https://www.${regionConfig?.domain}${detailLink}` : this.getOrderDetailUrl(id, region); headers.push({ id, orderId: id, date, total, detailUrl, platform: "amazon", region, }); } catch { // Skip malformed order cards continue; } } return headers; } /** * Get expected order count from the page. */ async getExpectedOrderCount(page: Page): Promise<number> { const countText = await getTextByXPaths( page, ['//span[@class="num-orders"]', '//*[contains(text(), "orders")]'], "", ); const match = countText.match(/(\d+)/); return match ? parseInt(match[1], 10) : 0; } /** * Extract order details from detail page. * Uses data-component selectors for 2024+ layouts with XPath fallbacks. */ async extractOrderDetails( page: Page, header: OrderHeader, ): Promise<OrderDetails> { const regionConfig = getRegionByCode(header.region); const currency = regionConfig?.currency || "USD"; // Navigate to detail page if needed const currentUrl = page.url(); if (!currentUrl.includes(header.id)) { await page.goto(header.detailUrl, { waitUntil: "domcontentloaded" }); // Wait for key content - prioritize data-component selectors await page .waitForSelector( '[data-component="orderDetails"], [data-component="shipments"], #od-subtotals, .order-details', { timeout: 2000 }, ) .catch(() => {}); } // Extract shipping - try data-component first, then XPath let shippingText = ""; const shippingEl = page .locator( '[data-component="orderSubtotals"] >> text=/Shipping|Postage|Delivery/i >> xpath=../following-sibling::*', ) .first(); if ((await shippingEl.count().catch(() => 0)) > 0) { shippingText = (await shippingEl.textContent({ timeout: 300 }).catch(() => "")) || ""; } if (!shippingText) { shippingText = await getTextByXPaths( page, [ '//div[contains(@id,"od-subtotals")]//span[contains(text(),"Shipping") or contains(text(),"Postage") or contains(text(),"Delivery")]/../following-sibling::div/span', ], "", ); } const shipping = parseMoney(shippingText, currency); // Extract tax (varies by region) - try data-component first let taxText = ""; const taxEl = page .locator( '[data-component="orderSubtotals"] >> text=/VAT|Tax|GST/i >> xpath=../following-sibling::*', ) .first(); if ((await taxEl.count().catch(() => 0)) > 0) { taxText = (await taxEl.textContent({ timeout: 300 }).catch(() => "")) || ""; } if (!taxText) { const taxXpaths = [ '//div[contains(@id,"od-subtotals")]//span[contains(text(),"VAT") or contains(text(),"Tax") or contains(text(),"GST")]/../following-sibling::div/span', '//span[contains(text(),"Estimated tax")]/../following-sibling::div/span', ]; taxText = await getTextByXPaths(page, taxXpaths, ""); } const tax = parseMoney(taxText, currency); // Extract recipient - try data-component selectors first let recipientName = ""; const recipientSelectors = [ '[data-component="shippingAddress"] .displayAddressFullName', '[data-component="shippingAddress"] ul li:first-child', '[data-component="deliveryAddress"] .displayAddressFullName', ]; for (const selector of recipientSelectors) { const el = page.locator(selector).first(); if ((await el.count().catch(() => 0)) > 0) { recipientName = (await el.textContent({ timeout: 300 }).catch(() => "")) || ""; if (recipientName) break; } } if (!recipientName) { recipientName = await getTextByXPaths( page, [ '//*[contains(@class,"displayAddressFullName")]', './/div[@data-component="shippingAddress"]/ul/li[1]', ], "", ); } // Extract payments - try data-component first let paymentText = ""; const paymentEl = page .locator( '[data-component="paymentInformation"], [data-component="viewPaymentPlanSummaryWidget"]', ) .first(); if ((await paymentEl.count().catch(() => 0)) > 0) { paymentText = (await paymentEl.textContent({ timeout: 300 }).catch(() => "")) || ""; } if (!paymentText) { paymentText = await getTextByXPaths( page, [ '//span[contains(text(),"ending in")]', '//div[contains(@class,"payment-method")]', ], "", ); } // Extract invoice URL const invoiceUrl = await page .locator('a[href*="/invoice"]') .first() .getAttribute("href") .catch(() => undefined); return { shipping, tax, recipient: { name: recipientName, }, payments: paymentText ? [{ method: paymentText }] : [], invoiceUrl: invoiceUrl ? `https://www.${regionConfig?.domain}${invoiceUrl}` : undefined, }; } /** * Extract items from order detail page. */ async extractItems(page: Page, header: OrderHeader): Promise<Item[]> { return extractItems(page, header); } /** * Extract shipments from order detail page. * @param fetchTrackingNumbers - If true, visits ship-track pages to get actual carrier tracking numbers (slower) */ async extractShipments( page: Page, header: OrderHeader, fetchTrackingNumbers = false, ): Promise<Shipment[]> { return extractShipments(page, header, { fetchTrackingNumbers }); } /** * Extract transactions from order detail page. */ async extractTransactions( page: Page, header: OrderHeader, ): Promise<Transaction[]> { return extractTransactions(page, header); } }

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/marcusquinn/amazon-order-history-csv-download-mcp'

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