search_components
Search for PC components like processors, graphics cards, and memory on meupc.net to compare prices across Brazilian stores and check specifications.
Instructions
Busca componentes de PC por texto no meupc.net (processadores, placas de vídeo, memórias, etc.)
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Texto para buscar componentes (ex: 'rtx 4070', 'ryzen 7') | |
| limit | No | Número máximo de resultados |
Implementation Reference
- src/tools/search-components.ts:12-56 (handler)Main handler function that searches for PC components on meupc.net. It fetches the search results page, parses HTML to extract component data (name, category, price, URL, image), and returns results as JSON string.export async function searchComponents(params: SearchComponentsParams): Promise<string> { const { query, limit } = params; const encoded = encodeURIComponent(query); const $ = await fetchPage(`/pesquisar?q=${encoded}`); const results: ComponentResult[] = []; $("div.media").each((_, el) => { if (results.length >= limit) return false; const $el = $(el); const nameEl = $el.find("div.media-content a h4"); const name = nameEl.text().trim(); if (!name) return; const url = $el.find("div.media-content > a").attr("href") ?? ""; const image = $el.find("div.media-left figure img").attr("src") ?? null; // Extrair categoria do link "Add na build" (ex: /processadores/add/HASH) const addLink = $el.find("a.button.is-link").attr("href") ?? ""; const categoryMatch = addLink.match(/meupc\.net\/([^/]+)\/add\//); const category = categoryMatch ? categoryMatch[1] : null; // Extrair preço do parágrafo de preço const priceText = $el.find("div.media-content > p").filter((_, p) => { return $(p).text().includes("R$"); }).first().text(); // Tentar pegar preço PIX primeiro, senão preço normal const pixMatch = priceText.match(/R\$\s*([\d.,]+)\s*no PIX/); const normalMatch = priceText.match(/R\$\s*([\d.,]+)/); const priceStr = pixMatch ? pixMatch[1] : normalMatch ? normalMatch[1] : null; const price = parsePrice(priceStr); results.push({ name, category, price, url: absoluteUrl(url), image: image && !image.includes("placeholder") ? absoluteUrl(image) : null, }); }); return JSON.stringify(results, null, 2); }
- src/tools/search-components.ts:5-10 (schema)Input validation schema using Zod that defines the tool parameters: 'query' (string, required) for the search text, and 'limit' (number, optional, default 10) for maximum results.export const searchComponentsSchema = z.object({ query: z.string().describe("Texto para buscar componentes (ex: 'rtx 4070', 'ryzen 7')"), limit: z.number().int().positive().default(10).describe("Número máximo de resultados"), }); export type SearchComponentsParams = z.infer<typeof searchComponentsSchema>;
- src/index.ts:16-23 (registration)MCP server tool registration that registers 'search_components' with its schema shape and handler function, including description in Portuguese.server.tool( "search_components", "Busca componentes de PC por texto no meupc.net (processadores, placas de vídeo, memórias, etc.)", searchComponentsSchema.shape, async (params) => ({ content: [{ type: "text", text: await searchComponents(params) }], }) );
- src/types.ts:1-7 (schema)Type definition for ComponentResult that the handler returns - includes name, category, price, URL, and image fields.export interface ComponentResult { name: string; category: string | null; price: number | null; url: string; image: string | null; }
- src/scraper.ts:11-49 (helper)Helper utility functions used by the handler: fetchPage() for web scraping, absoluteUrl() for URL resolution, and parsePrice() for extracting numeric prices from text.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; }