Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 18 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
{
"name": "mcp-server-amazon",
"private": true,
"description": "Amazon Model Context Protocol Server",
"description": "Amazon Model Context Protocol Server - enables AI assistants to interact with Amazon services",
"version": "1.0.0",
"main": "./build/index.js",
"type": "module",
"bin": {
"mcp-server-amazon": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod -R 755 ./build",
"clean": "rm mocks/*_202*"
"build": "tsc",
"clean": "rm mocks/*_202*",
"prepare": "npm run build"
},
"files": [
"build"
"build",
"amazonCookies.example.json"
],
"keywords": [
"mcp",
"amazon",
"model-context-protocol",
"ai",
"scraping",
"puppeteer"
],
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.0",
"cheerio": "^1.1.0",
Expand Down
9 changes: 4 additions & 5 deletions src/cart.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as cheerio from 'cheerio'
import fs from 'fs'
import { USE_MOCKS, EXPORT_LIVE_SCRAPING_FOR_MOCKS, getAmazonDomain } from './config.js'
import path from 'path'
import { USE_MOCKS, EXPORT_LIVE_SCRAPING_FOR_MOCKS, PACKAGE_ROOT, getAmazonDomain } from './config.js'
import { createBrowserAndPage, getTimestamp, throwIfNotLoggedIn } from './utils.js'

const __dirname = new URL('.', import.meta.url).pathname

// ##################################
// Cart Content Types
// ##################################
Expand Down Expand Up @@ -35,7 +34,7 @@ export async function getCartContent(): Promise<CartContent> {
let html: string
if (USE_MOCKS) {
console.error('[INFO][get-cart-content] Fetching cart content from mocks')
const mockPath = `${__dirname}/../mocks/getCartContent.html`
const mockPath = path.join(PACKAGE_ROOT, 'mocks', 'getCartContent.html')
html = fs.readFileSync(mockPath, 'utf-8')
} else {
const domain = getAmazonDomain()
Expand All @@ -61,7 +60,7 @@ export async function getCartContent(): Promise<CartContent> {
if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) {
// Export only the `#sc-active-cart` content to a mock file
const timestamp = getTimestamp()
const mockPath = `${__dirname}/../mocks/getCartContent_${timestamp}.html`
const mockPath = path.join(PACKAGE_ROOT, 'mocks', `getCartContent_${timestamp}.html`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mocks doesn't exist when I installed with npx, so this throws. maybe EXPORT_LIVE_SCRAPING_FOR_MOCKS should be disabled by default

[ERROR][get-cart-content] Error in get-cart-content tool: Error: ENOENT: no such file or directory, open '/home/nanobot/.npm/_npx/91ea6e9f96498d68/node_modules/mcp-server-amazon/mocks/getCartContent_2026-04-03_02-54-11.html'
    at Object.writeFileSync (node:fs:2398:20)
    at getCartContent (file:///home/nanobot/.npm/_npx/91ea6e9f96498d68/node_modules/mcp-server-amazon/build/cart.js:38:20)
    at async file:///home/nanobot/.npm/_npx/91ea6e9f96498d68/node_modules/mcp-server-amazon/build/index.js:51:23
    at async McpServer.executeToolHandler (file:///home/nanobot/.npm/_npx/91ea6e9f96498d68/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:233:20)
    at async file:///home/nanobot/.npm/_npx/91ea6e9f96498d68/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:32
    at async wrappedHandler (file:///home/nanobot/.npm/_npx/91ea6e9f96498d68/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js:125:32) {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/home/nanobot/.npm/_npx/91ea6e9f96498d68/node_modules/mcp-server-amazon/mocks/getCartContent_2026-04-03_02-54-11.html'
}

const cartHtml = await page.$eval('#sc-active-cart', el => el.outerHTML)
fs.writeFileSync(mockPath, cartHtml)
console.error(`[INFO][get-cart-content] Exported cart container HTML to ${mockPath}`)
Expand Down
23 changes: 18 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import path from 'path'
import { fileURLToPath } from 'url'
import { loadAmazonCookiesFile } from './utils.js'

const __dirname = new URL('.', import.meta.url).pathname
const __dirname = path.dirname(fileURLToPath(import.meta.url))

export const IS_BROWSER_VISIBLE = false
/** Directory where the package is installed (one level up from build/) */
export const PACKAGE_ROOT = path.resolve(__dirname, '..')

export const IS_BROWSER_VISIBLE = process.env.IS_BROWSER_VISIBLE === 'true'

/** Use local mock files instead of live scraping */
export const USE_MOCKS = false
export const USE_MOCKS = process.env.USE_MOCK_RESPONSES === 'true'

/** Export live scraping HTML to mocks for future use */
export const EXPORT_LIVE_SCRAPING_FOR_MOCKS = true
export const EXPORT_LIVE_SCRAPING_FOR_MOCKS = process.env.EXPORT_LIVE_SCRAPING_FOR_MOCKS !== 'false'

export const COOKIES_FILE_PATH = `${__dirname}/../amazonCookies.json`
/**
* Cookie file path resolution order:
* 1. AMAZON_COOKIES_PATH env var (absolute or relative to CWD)
* 2. amazonCookies.json in current working directory
* 3. amazonCookies.json in the package root directory
*/
export const COOKIES_FILE_PATH =
process.env.AMAZON_COOKIES_PATH ||
path.join(process.cwd(), 'amazonCookies.json')
/**
* Go to the Amazon website and log in to your account
* Then export cookies as JSON using a browser extension like "Cookie-Editor"
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
Expand Down
9 changes: 4 additions & 5 deletions src/orders.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as cheerio from 'cheerio'
import fs from 'fs'
import path from 'path'
import puppeteer from 'puppeteer'
import { USE_MOCKS, EXPORT_LIVE_SCRAPING_FOR_MOCKS, getAmazonDomain } from './config.js'
import { USE_MOCKS, EXPORT_LIVE_SCRAPING_FOR_MOCKS, PACKAGE_ROOT, getAmazonDomain } from './config.js'
import { createBrowserAndPage, getTimestamp, throwIfNotLoggedIn } from './utils.js'

const __dirname = new URL('.', import.meta.url).pathname

// ##################################
// Get Orders History
// ##################################
Expand All @@ -14,7 +13,7 @@ export async function getOrdersHistory() {
let html: string
if (USE_MOCKS) {
console.error('[INFO][get-orders-history] Fetching orders history from mocks')
const mockPath = `${__dirname}/../mocks/getOrdersHistory.html`
const mockPath = path.join(PACKAGE_ROOT, 'mocks', 'getOrdersHistory.html')
html = fs.readFileSync(mockPath, 'utf-8')
} else {
const domain = getAmazonDomain()
Expand Down Expand Up @@ -42,7 +41,7 @@ export async function getOrdersHistory() {
if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) {
// Export only the .order-card and .your-orders-content-container content to a mock file
const timestamp = getTimestamp()
const mockPath = `${__dirname}/../mocks/getOrdersHistory_${timestamp}.html`
const mockPath = path.join(PACKAGE_ROOT, 'mocks', `getOrdersHistory_${timestamp}.html`)
const orderCardsHtml = await page.$$eval('.order-card, .your-orders-content-container', elements =>
elements.map(el => el.outerHTML).join('\n')
)
Expand Down
17 changes: 8 additions & 9 deletions src/products.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as cheerio from 'cheerio'
import fs from 'fs'
import path from 'path'
import puppeteer from 'puppeteer'
import { USE_MOCKS, EXPORT_LIVE_SCRAPING_FOR_MOCKS, getAmazonDomain } from './config.js'
import { USE_MOCKS, EXPORT_LIVE_SCRAPING_FOR_MOCKS, PACKAGE_ROOT, getAmazonDomain } from './config.js'
import { createBrowserAndPage, getTimestamp, throwIfNotLoggedIn } from './utils.js'

const __dirname = new URL('.', import.meta.url).pathname

// ##################################
// Product Details
// ##################################
Expand Down Expand Up @@ -39,7 +38,7 @@ export async function getProductDetails(asin: string): Promise<ProductDetails> {
let html: string
if (USE_MOCKS) {
console.error('[INFO][get-product-details] Fetching product details from mocks')
const mockPath = `${__dirname}/../mocks/getProductDetails.html`
const mockPath = path.join(PACKAGE_ROOT, 'mocks', 'getProductDetails.html')
html = fs.readFileSync(mockPath, 'utf-8')
} else {
const domain = getAmazonDomain()
Expand All @@ -65,7 +64,7 @@ export async function getProductDetails(asin: string): Promise<ProductDetails> {
if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) {
// Export the main product content to a mock file
const timestamp = getTimestamp()
const mockPath = `${__dirname}/../mocks/getProductDetails_${timestamp}.html`
const mockPath = path.join(PACKAGE_ROOT, 'mocks', `getProductDetails_${timestamp}.html`)
const productHtml = await page.content()
fs.writeFileSync(mockPath, productHtml)
console.error(`[INFO][get-product-details] Exported product page HTML to ${mockPath}`)
Expand Down Expand Up @@ -136,15 +135,15 @@ async function extractProductDetailsPageData($: cheerio.CheerioAPI, asin: string
if (mainImageUrl) {
if (USE_MOCKS) {
console.error('[INFO][get-product-details] Downloading product main image from mocks')
const mockPath = `${__dirname}/../mocks/getProductDetails_image_base64.txt`
const mockPath = path.join(PACKAGE_ROOT, 'mocks', 'getProductDetails_image_base64.txt')
mainImageBase64 = fs.readFileSync(mockPath, 'utf-8')
} else {
// FIXME: This is not supported yet by Claude Desktop client!! Uncomment when they implement it
// console.error(`[INFO][get-product-details] Downloading main image from ${mainImageUrl}`)
// mainImageBase64 = await downloadImageAsBase64(mainImageUrl)
// if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) {
// const timestamp = getTimestamp()
// const mockPath = `${__dirname}/../mocks/getProductDetails_image_base64_${timestamp}.txt`
// const mockPath = path.join(PACKAGE_ROOT, 'mocks', `getProductDetails_image_base64_${timestamp}.txt`)
// fs.writeFileSync(mockPath, mainImageBase64)
// console.error(`[INFO][get-product-details] Exported main image base64 to ${mockPath}`)
// }
Expand Down Expand Up @@ -204,7 +203,7 @@ export async function searchProducts(searchTerm: string): Promise<ProductSearchR
let html: string
if (USE_MOCKS) {
console.error('[INFO][search-products] Fetching search results from mocks')
const mockPath = `${__dirname}/../mocks/searchProducts.html`
const mockPath = path.join(PACKAGE_ROOT, 'mocks', 'searchProducts.html')
html = fs.readFileSync(mockPath, 'utf-8')
} else {
const domain = getAmazonDomain()
Expand Down Expand Up @@ -234,7 +233,7 @@ export async function searchProducts(searchTerm: string): Promise<ProductSearchR
const timestamp = getTimestamp()
const searchResultsHtml = await page.$eval('.s-search-results', el => el.outerHTML)
const mockFileName = `searchProducts_${timestamp}.html`
const mockPath = `${__dirname}/../mocks/${mockFileName}`
const mockPath = path.join(PACKAGE_ROOT, 'mocks', mockFileName)
fs.writeFileSync(mockPath, searchResultsHtml)
console.error(`[INFO][search-products] Exported search results HTML to ${mockPath}`)
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"noEmitOnError": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "src/**/*.test.ts"]
}