search_components
Search for PC components on meupc.net using a text query. Results include processors, graphics cards, memory, and more.
Instructions
Busca componentes de PC por texto no meupc.net (processadores, placas de vídeo, memórias, etc.)
Input 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-53 (handler)Main handler function for the 'search_components' tool. It scrapes meupc.net search results by querying /pesquisar?q=..., parsing each result's name, category (from 'Add na build' link), price (PIX or normal), URL, and image. Returns JSON string of up to 'limit' results.
export async function searchComponents(params: SearchComponentsParams): Promise<string> { const { query, limit } = params; const encoded = encodeURIComponent(query); const root = await fetchPage(`/pesquisar?q=${encoded}`); const results: ComponentResult[] = []; for (const el of root.querySelectorAll("div.media")) { if (results.length >= limit) break; const name = el.querySelector("div.media-content a h4")?.text.trim() ?? ""; if (!name) continue; const url = el.querySelector("div.media-content > a")?.getAttribute("href") ?? ""; const image = el.querySelector("div.media-left figure img")?.getAttribute("src") ?? null; // Extrair categoria do link "Add na build" (ex: /processadores/add/HASH) const addLink = el.querySelector("a.button.is-link")?.getAttribute("href") ?? ""; const categoryMatch = addLink.match(/meupc\.net\/([^/]+)\/add\//); const category = categoryMatch ? categoryMatch[1] : null; // Extrair preço do parágrafo de preço const priceP = el.querySelectorAll("div.media-content > p").find(p => p.text.includes("R$")); const priceText = priceP?.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-8 (schema)Zod schema defining the input for 'search_components': 'query' (string, required) and 'limit' (positive int, default 10).
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"), }); - src/index.ts:16-23 (registration)Registration of the 'search_components' tool with the MCP server using server.tool(...). Provides description, schema.shape, and an async callback that calls the handler.
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 (helper)The ComponentResult interface used as the return structure for search results (name, category, price, url, image).
export interface ComponentResult { name: string; category: string | null; price: number | null; url: string; image: string | null; } - src/scraper.ts:10-48 (helper)fetchPage helper used by the handler to fetch and parse HTML from meupc.net. Also includes absoluteUrl and parsePrice utilities.
export async function fetchPage(path: string): Promise<HTMLElement> { 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 parse(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; }