import { logger } from "../logger"; import { tools } from "./tools"; const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:4005/api"; const API_TIMEOUT = Number(process.env.API_TIMEOUT) || 180000; async function callMarketplaceApi( marketplace: string, params: URLSearchParams, ): Promise { const url = `${API_BASE_URL}/${marketplace}?${params.toString()}`; logger.log(`[MCP] Calling ${marketplace} API`); const response = await Promise.race([ fetch(url), new Promise((_, reject) => setTimeout( () => reject(new Error(`Request timed out after ${API_TIMEOUT}ms`)), API_TIMEOUT, ), ), ]); if (!response.ok) { const errorText = await response.text(); logger.error( `[MCP] ${marketplace} API error ${response.status}: ${errorText}`, ); throw new Error(`API returned ${response.status}: ${errorText}`); } return response.json(); } /** * Handle MCP JSON-RPC 2.0 protocol requests */ export async function handleMcpRequest(req: Request): Promise { try { const body = await req.json(); // Validate JSON-RPC 2.0 format if (!body.jsonrpc || body.jsonrpc !== "2.0" || !body.method) { return Response.json( { jsonrpc: "2.0", error: { code: -32600, message: "Invalid Request" }, id: body.id, }, { status: 400 }, ); } const { method, params, id } = body; // Handle initialize method if (method === "initialize") { return Response.json({ jsonrpc: "2.0", id, result: { protocolVersion: "2025-06-18", capabilities: { tools: { listChanged: true, }, }, serverInfo: { name: "marketplace-scrapers", version: "1.0.0", }, instructions: "Use search_kijiji, search_facebook, or search_ebay tools to find listings across Canadian marketplaces", }, }); } // Handle tools/list method if (method === "tools/list") { return Response.json({ jsonrpc: "2.0", id, result: { tools, }, }); } // Handle notifications (messages without id field should not get a response) if (!id) { // Notifications don't require a response if (method === "notifications/initialized") { // Client initialized successfully, no response needed return new Response(null, { status: 204 }); } if (method === "notifications/progress") { // Progress notifications, no response needed return new Response(null, { status: 204 }); } // Unknown notification - still no response for notifications return new Response(null, { status: 204 }); } // Handle tools/call method if (method === "tools/call") { const { name, arguments: args } = params || {}; if (!name || !args) { return Response.json( { jsonrpc: "2.0", id, error: { code: -32602, message: "Invalid params: name and arguments required", }, }, { status: 400 }, ); } // Route tool calls to appropriate handlers try { let result: unknown; if (name === "search_kijiji") { const query = args.query; if (!query) { return Response.json({ jsonrpc: "2.0", id, error: { code: -32602, message: "query parameter is required" }, }); } const params = new URLSearchParams({ q: query }); if (args.location) params.append("location", args.location); if (args.category) params.append("category", args.category); if (args.keywords) params.append("keywords", args.keywords); if (args.sortBy) params.append("sortBy", args.sortBy); if (args.sortOrder) params.append("sortOrder", args.sortOrder); if (args.maxPages) params.append("maxPages", args.maxPages.toString()); if (args.priceMin) params.append("priceMin", args.priceMin.toString()); if (args.priceMax) params.append("priceMax", args.priceMax.toString()); if (args.unstableFilter !== undefined) params.append("unstableFilter", args.unstableFilter.toString()); logger.log( `[MCP] Calling Kijiji API: ${API_BASE_URL}/kijiji?${params.toString()}`, ); const response = await Promise.race([ fetch(`${API_BASE_URL}/kijiji?${params.toString()}`), new Promise((_, reject) => setTimeout( () => reject(new Error(`Request timed out after ${API_TIMEOUT}ms`)), API_TIMEOUT, ), ), ]); if (!response.ok) { const errorText = await response.text(); logger.error( `[MCP] Kijiji API error ${response.status}: ${errorText}`, ); let errorMessage = `API returned ${response.status}: ${errorText}`; try { const errorJson = JSON.parse(errorText) as { message?: string }; if (errorJson.message) errorMessage = errorJson.message; } catch { // not JSON — use raw text } throw new Error(errorMessage); } result = await response.json(); logger.log( `[MCP] Kijiji returned ${Array.isArray(result) ? result.length : 0} items`, ); } else if (name === "search_facebook") { const query = args.query; if (!query) { return Response.json({ jsonrpc: "2.0", id, error: { code: -32602, message: "query parameter is required" }, }); } const params = new URLSearchParams({ q: query }); if (args.location) params.append("location", args.location); if (args.maxItems) params.append("maxItems", args.maxItems.toString()); if (args.unstableFilter !== undefined) params.append("unstableFilter", args.unstableFilter.toString()); result = await callMarketplaceApi("facebook", params); } else if (name === "search_ebay") { const query = args.query; if (!query) { return Response.json({ jsonrpc: "2.0", id, error: { code: -32602, message: "query parameter is required" }, }); } const params = new URLSearchParams({ q: query }); if (args.minPrice) params.append("minPrice", args.minPrice.toString()); if (args.maxPrice) params.append("maxPrice", args.maxPrice.toString()); if (args.strictMode !== undefined) params.append("strictMode", args.strictMode.toString()); if (args.exclusions?.length) params.append("exclusions", args.exclusions.join(",")); if (args.keywords?.length) params.append("keywords", args.keywords.join(",")); if (args.buyItNowOnly !== undefined) params.append("buyItNowOnly", args.buyItNowOnly.toString()); if (args.canadaOnly !== undefined) params.append("canadaOnly", args.canadaOnly.toString()); if (args.maxItems) params.append("maxItems", args.maxItems.toString()); if (args.unstableFilter !== undefined) params.append("unstableFilter", args.unstableFilter.toString()); result = await callMarketplaceApi("ebay", params); } else { return Response.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown tool: ${name}` }, }); } return Response.json({ jsonrpc: "2.0", id, result: { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; return Response.json({ jsonrpc: "2.0", id, error: { code: -32603, message: `Tool execution failed: ${errorMessage}`, }, }); } } // Method not found return Response.json( { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` }, }, { status: 404 }, ); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; return Response.json( { jsonrpc: "2.0", error: { code: -32700, message: `Parse error: ${errorMessage}` }, }, { status: 400 }, ); } }