get_component_details
Retrieve technical specifications and compare prices across Brazilian stores for PC components using a product URL.
Instructions
Detalhes completos de um componente: especificações técnicas, preços por loja (PIX e normal), menor preço
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | URL do componente (ex: '/peca/W6UIQZ/processador-amd-ryzen-7-5800x3d' ou URL completa) |
Implementation Reference
- Main handler function getComponentDetails that fetches and parses component details from meupc.net. Extracts name, category, image, technical specifications, prices from multiple stores, and calculates the lowest price. Returns a JSON string with all component details.export async function getComponentDetails(params: GetComponentDetailsParams): Promise<string> { const { url } = params; const $ = await fetchPage(url); const name = $("h1.title").first().text().trim(); // Extrair categoria do breadcrumb const breadcrumbs = $("nav.breadcrumb li a"); let category: string | null = null; breadcrumbs.each((_, el) => { const href = $(el).attr("href") ?? ""; if (href.match(/meupc\.net\/\w/) && !href.includes("/peca/")) { category = $(el).text().trim(); } }); // Imagem principal const image = $("div.glide ul.glide__slides li.glide__slide figure img").first().attr("src") ?? $("figure.image img").first().attr("src") ?? null; // Especificações técnicas (primeira table.table.is-striped) const specs: Record<string, string> = {}; $("table.table.is-striped tr").each((_, row) => { const label = $(row).find("th").text().trim(); if (!label) return; const td = $(row).find("td"); // Verificar valores booleanos (ícones sim/não) const yesIcon = td.find('span.icon[title="Sim"]'); const noIcon = td.find('span.icon[title="Não"], span.icon[title="Nao"], span.icon[title="Não/Nenhum"], span.icon[title="Nao/Nenhum"]'); let value: string; if (yesIcon.length > 0) { value = "Sim"; } else if (noIcon.length > 0) { value = "Não"; } else { value = td.text().trim(); } if (label && value) { specs[label] = value; } }); // Preços por loja (table.table.is-responsive) const prices: StorePrice[] = []; $("table.table.is-responsive tbody tr").each((_, row) => { const $row = $(row); const storeImg = $row.find("th a.loja-img img"); const store = storeImg.attr("alt")?.trim() ?? storeImg.attr("title")?.trim() ?? ""; if (!store) return; const priceTd = $row.find('td[data-label="Preco"], td.table-responsive-fullwidth.has-text-right'); const priceText = priceTd.find("a.has-text-weight-bold").first().text().trim(); const price = parsePrice(priceText); const pixTd = $row.find('td[data-label="Preco PIX"]'); const pixText = pixTd.find("a.has-text-weight-bold").first().text().trim(); const pricePix = parsePrice(pixText); const buyLink = $row.find("a.button.is-buy").attr("href") ?? null; // Verificar disponibilidade — se não tem preço, não está disponível const available = price !== null || pricePix !== null; prices.push({ store, price: price ?? 0, pricePix, url: buyLink ? absoluteUrl(buyLink) : null, available, }); }); // Menor preço (PIX se disponível, senão normal) const allPrices = prices .filter(p => p.available) .flatMap(p => [p.pricePix, p.price].filter((v): v is number => v !== null && v > 0)); const lowestPrice = allPrices.length > 0 ? Math.min(...allPrices) : null; const result: ComponentDetails = { name, category, specs, prices, lowestPrice, url: absoluteUrl(url), image: image ? absoluteUrl(image) : null, }; return JSON.stringify(result, null, 2); }
- Zod schema definition for input validation. getComponentDetailsSchema defines a single 'url' parameter accepting either a relative path or full URL to a component page.export const getComponentDetailsSchema = z.object({ url: z.string().describe("URL do componente (ex: '/peca/W6UIQZ/processador-amd-ryzen-7-5800x3d' ou URL completa)"), }); export type GetComponentDetailsParams = z.infer<typeof getComponentDetailsSchema>;
- src/index.ts:34-41 (registration)MCP tool registration for 'get_component_details'. Registers the handler with the server using the schema shape and async handler that returns the result as text content.server.tool( "get_component_details", "Detalhes completos de um componente: especificações técnicas, preços por loja (PIX e normal), menor preço", getComponentDetailsSchema.shape, async (params) => ({ content: [{ type: "text", text: await getComponentDetails(params) }], }) );
- src/scraper.ts:11-49 (helper)Helper utilities used by getComponentDetails: fetchPage (fetches and parses HTML), absoluteUrl (converts relative URLs to absolute), and parsePrice (extracts numeric values from price strings).export async function fetchPage(path: string): Promise<CheerioAPI> { const url = path.startsWith("http") ? path : `${BASE_URL}${path.startsWith("/") ? "" : "/"}${path}`; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT); try { const response = await fetch(url, { headers: { "User-Agent": USER_AGENT, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", }, signal: controller.signal, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText} — ${url}`); } const html = await response.text(); return cheerio.load(html); } finally { clearTimeout(timeout); } } export function absoluteUrl(path: string | undefined | null): string { if (!path) return ""; if (path.startsWith("http")) return path; return `${BASE_URL}${path.startsWith("/") ? "" : "/"}${path}`; } export function parsePrice(text: string | undefined | null): number | null { if (!text) return null; const cleaned = text.replace(/[R$\s.]/g, "").replace(",", "."); const num = parseFloat(cleaned); return isNaN(num) ? null : num; }
- src/types.ts:9-25 (schema)Type definitions for ComponentDetails and StorePrice interfaces that define the structure of the data returned by getComponentDetails handler.export interface ComponentDetails { name: string; category: string | null; specs: Record<string, string>; prices: StorePrice[]; lowestPrice: number | null; url: string; image: string | null; } export interface StorePrice { store: string; price: number; pricePix: number | null; url: string | null; available: boolean; }