Skip to main content
Glama
amazon.ts6.47 kB
// src/providers/amazon.ts import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; /** ---------- Types ---------- */ export type Filters = { category?: string; brand?: string; budgetMaxINR?: number; minRating?: number; }; export type FeaturePreference = "camera" | "battery" | "display" | "performance"; export type Product = { id: string; title: string; brand?: string; category?: string; priceINR: number; rating: number; ratingCount?: number; specs?: Record<string, string>; images?: string[]; url?: string; }; export type Review = { productId: string; stars: number; title?: string; text?: string; aspect?: "camera" | "battery" | "display" | "performance" | string; }; /** ---------- Resolve data paths relative to this file (works in dist/ & src/) ---------- */ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // ../../data from dist/providers/* or src/providers/* both converge on repo-root/data const DATA_DIR = path.resolve(__dirname, "../../data"); const CATALOG_PATH = path.join(DATA_DIR, "catalog.sample.json"); const REVIEWS_PATH = path.join(DATA_DIR, "reviews.sample.json"); /** ---------- Load helpers with safe fallback ---------- */ function safeReadJSON<T>(p: string, fallback: T): T { try { const raw = fs.readFileSync(p, "utf8"); return JSON.parse(raw) as T; } catch { return fallback; } } /** ---------- Fallback datasets (keeps dev flowing if files missing) ---------- */ const FALLBACK_CATALOG: Product[] = [ { id: "B00PHONE001", title: "CamX Pro 5G Smartphone", brand: "CamX", category: "mobiles", priceINR: 13999, rating: 4.3, ratingCount: 12453, specs: { camera: "50MP OIS main + 8MP ultra-wide", battery: "5000 mAh", display: '6.5" AMOLED 120Hz', storage: "128GB", ram: "6GB", }, images: [ "https://example.com/img/camxpro-front.jpg", "https://example.com/img/camxpro-back.jpg", ], url: "https://www.amazon.in/dp/B00PHONE001", }, ]; const FALLBACK_REVIEWS: Record<string, Review[]> = { B00PHONE001: [ { productId: "B00PHONE001", stars: 5, title: "Great camera!", text: "OIS helps a lot in night photos.", aspect: "camera", }, { productId: "B00PHONE001", stars: 4, title: "Good display", text: "120Hz feels smooth.", aspect: "display", }, ], }; /** ---------- Load data ---------- */ const CATALOG: Product[] = safeReadJSON<Product[]>(CATALOG_PATH, FALLBACK_CATALOG); const REVIEWS_INDEX: Record<string, Review[]> = safeReadJSON<Record<string, Review[]>>( REVIEWS_PATH, FALLBACK_REVIEWS ); /** ---------- Scoring for BudgetTop ---------- */ export function scoreProductForBudget( p: Product, featurePref?: FeaturePreference, budgetMaxINR?: number ) { // Base score from rating (0..5 -> 0..5) const rating = typeof p.rating === "number" ? p.rating : 0; // Feature bonus (light heuristic) let featureBonus = 0; if (featurePref) { const specText = (p.specs?.camera || p.specs?.battery || p.specs?.display || "") + " " + (p.title || ""); const lower = specText.toLowerCase(); const pref = featurePref.toLowerCase(); // very light-weight match bonus if (lower.includes(pref)) featureBonus += 0.6; // special hints if (featurePref === "camera" && /ois|mp|ultra\-wide|telephoto/.test(lower)) featureBonus += 0.2; if (featurePref === "battery" && /5000|6000|mah|fast\s*charge/.test(lower)) featureBonus += 0.2; if (featurePref === "display" && /amoled|oled|120hz|90hz|hdr/.test(lower)) featureBonus += 0.2; if (featurePref === "performance" && /snapdragon|dimensity|8gb|12gb/.test(lower)) featureBonus += 0.2; } // Price penalty if over budget let pricePenalty = 0; if (typeof budgetMaxINR === "number" && p.priceINR > budgetMaxINR) { // penalize proportionally to how far over budget const over = p.priceINR - budgetMaxINR; pricePenalty = Math.min(1.5, over / Math.max(1, budgetMaxINR) * 2); // cap } // Final score const score = rating + featureBonus - pricePenalty; return { rating, featureBonus, pricePenalty, score }; } /** ---------- Provider ---------- */ export class AmazonProvider { private catalog: Product[]; private reviewsIndex: Record<string, Review[]>; constructor() { this.catalog = CATALOG; this.reviewsIndex = REVIEWS_INDEX; } /** Simple search with optional filters */ search(query: string, filters: Filters = {}) { const q = (query || "").trim().toLowerCase(); let results = this.catalog.filter((p) => { const hay = (p.title || "") + " " + (p.brand || "") + " " + Object.values(p.specs || {}).join(" "); const matchQ = q ? hay.toLowerCase().includes(q) : true; const matchCategory = filters.category ? (p.category || "").toLowerCase() === filters.category.toLowerCase() : true; const matchBrand = filters.brand ? (p.brand || "").toLowerCase() === filters.brand.toLowerCase() : true; const matchBudget = typeof filters.budgetMaxINR === "number" ? p.priceINR <= filters.budgetMaxINR : true; const matchRating = typeof filters.minRating === "number" ? p.rating >= filters.minRating : true; return matchQ && matchCategory && matchBrand && matchBudget && matchRating; }); // naive sort: better rating first, then lower price results = results.sort((a, b) => { if (b.rating !== a.rating) return b.rating - a.rating; return a.priceINR - b.priceINR; }); return { total: results.length, products: results, }; } /** Get a single product’s details */ details(productId: string) { const p = this.catalog.find((x) => x.id === productId); if (!p) throw new Error(`Product not found: ${productId}`); const revs = this.reviewsIndex[productId] || []; const avg = revs.length > 0 ? revs.reduce((s, r) => s + (r.stars || 0), 0) / revs.length : null; return { ...p, reviewCount: revs.length, avgReviewRating: avg, }; } /** Get reviews for a product */ getReviews(productId: string, limit = 10): Review[] { const revs = this.reviewsIndex[productId] || []; return revs.slice(0, Math.max(0, limit)); } }

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/rochitl72/mcp-den'

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