fix: resolve biome lint errors and warnings

This commit is contained in:
2026-01-23 10:33:15 -05:00
parent 441ff436c4
commit 637f1a4e75
14 changed files with 2194 additions and 2177 deletions

View File

@@ -1,34 +1,34 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"vcs": { "vcs": {
"enabled": true, "enabled": true,
"clientKind": "git", "clientKind": "git",
"useIgnoreFile": true "useIgnoreFile": true
}, },
"files": { "files": {
"includes": ["**", "!!**/dist"] "includes": ["**", "!!**/dist"]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"indentStyle": "space" "indentStyle": "space"
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true "recommended": true
} }
}, },
"javascript": { "javascript": {
"formatter": { "formatter": {
"quoteStyle": "double" "quoteStyle": "double"
} }
}, },
"assist": { "assist": {
"enabled": true, "enabled": true,
"actions": { "actions": {
"source": { "source": {
"organizeImports": "on" "organizeImports": "on"
} }
} }
} }
} }

View File

@@ -6,7 +6,9 @@
}, },
"private": true, "private": true,
"type": "module", "type": "module",
"workspaces": ["packages/*"], "workspaces": [
"packages/*"
],
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.3.11" "@biomejs/biome": "2.3.11"
} }

View File

@@ -2,5 +2,5 @@
* Health check endpoint * Health check endpoint
*/ */
export function statusRoute(): Response { export function statusRoute(): Response {
return new Response("OK", { status: 200 }); return new Response("OK", { status: 200 });
} }

View File

@@ -1,45 +1,42 @@
// Export all scrapers // Export all scrapers
export {
default as fetchKijijiItems,
slugify,
resolveLocationId,
resolveCategoryId,
buildSearchUrl,
extractApolloState,
parseSearch,
parseDetailedListing,
HttpError,
NetworkError,
ParseError,
RateLimitError,
ValidationError,
} from "./scrapers/kijiji";
export type {
KijijiListingDetails,
DetailedListing,
SearchOptions,
ListingFetchOptions,
} from "./scrapers/kijiji";
export {
default as fetchFacebookItems,
fetchFacebookItem,
parseFacebookCookieString,
ensureFacebookCookies,
extractFacebookMarketplaceData,
extractFacebookItemData,
parseFacebookAds,
parseFacebookItem,
} from "./scrapers/facebook";
export type { FacebookListingDetails } from "./scrapers/facebook";
export { default as fetchEbayItems } from "./scrapers/ebay";
export type { EbayListingDetails } from "./scrapers/ebay"; export type { EbayListingDetails } from "./scrapers/ebay";
export { default as fetchEbayItems } from "./scrapers/ebay";
// Export shared utilities export type { FacebookListingDetails } from "./scrapers/facebook";
export * from "./utils/http"; export {
export * from "./utils/delay"; default as fetchFacebookItems,
export * from "./utils/format"; ensureFacebookCookies,
extractFacebookItemData,
extractFacebookMarketplaceData,
fetchFacebookItem,
parseFacebookAds,
parseFacebookCookieString,
parseFacebookItem,
} from "./scrapers/facebook";
export type {
DetailedListing,
KijijiListingDetails,
ListingFetchOptions,
SearchOptions,
} from "./scrapers/kijiji";
export {
buildSearchUrl,
default as fetchKijijiItems,
extractApolloState,
HttpError,
NetworkError,
ParseError,
parseDetailedListing,
parseSearch,
RateLimitError,
resolveCategoryId,
resolveLocationId,
slugify,
ValidationError,
} from "./scrapers/kijiji";
// Export shared types // Export shared types
export * from "./types/common"; export * from "./types/common";
export * from "./utils/delay";
export * from "./utils/format";
// Export shared utilities
export * from "./utils/http";

View File

@@ -1,9 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { parseHTML } from "linkedom";
import type { HTMLString } from "../types/common";
import { delay } from "../utils/delay";
import { formatCentsToCurrency } from "../utils/format";
import { isRecord } from "../utils/http";
// ----------------------------- Types ----------------------------- // ----------------------------- Types -----------------------------
@@ -43,7 +38,7 @@ function parseEbayPrice(
const amountStr = numberMatches[0].replace(/,/g, ""); const amountStr = numberMatches[0].replace(/,/g, "");
const dollars = parseFloat(amountStr); const dollars = parseFloat(amountStr);
if (isNaN(dollars)) return null; if (Number.isNaN(dollars)) return null;
const cents = Math.round(dollars * 100); const cents = Math.round(dollars * 100);
@@ -185,8 +180,7 @@ function parseEbayListings(
const text = el.textContent?.trim(); const text = el.textContent?.trim();
// Must contain $, be reasonably short (price shouldn't be paragraph), and not contain product words // Must contain $, be reasonably short (price shouldn't be paragraph), and not contain product words
if ( if (
text && text?.includes("$") &&
text.includes("$") &&
text.length < 100 && text.length < 100 &&
!text.includes("laptop") && !text.includes("laptop") &&
!text.includes("computer") && !text.includes("computer") &&

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,18 +3,18 @@ export type HTMLString = string;
/** Currency price object with formatting options */ /** Currency price object with formatting options */
export interface Price { export interface Price {
amountFormatted: string; amountFormatted: string;
cents: number; cents: number;
currency: string; currency: string;
} }
/** Base listing details common across all marketplaces */ /** Base listing details common across all marketplaces */
export interface ListingDetails { export interface ListingDetails {
url: string; url: string;
title: string; title: string;
listingPrice: Price; listingPrice: Price;
listingType: string; listingType: string;
listingStatus: string; listingStatus: string;
address?: string | null; address?: string | null;
creationDate?: string; creationDate?: string;
} }

View File

@@ -4,5 +4,5 @@
* @returns A promise that resolves after the specified delay * @returns A promise that resolves after the specified delay
*/ */
export function delay(ms: number): Promise<void> { export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }

View File

@@ -4,18 +4,21 @@
* @param locale - Locale string for formatting (e.g., 'en-CA', 'en-US') * @param locale - Locale string for formatting (e.g., 'en-CA', 'en-US')
* @returns Formatted currency string * @returns Formatted currency string
*/ */
export function formatCentsToCurrency(cents: number, locale: string = "en-CA"): string { export function formatCentsToCurrency(
try { cents: number,
const formatter = new Intl.NumberFormat(locale, { locale: string = "en-CA",
style: "currency", ): string {
currency: "CAD", try {
minimumFractionDigits: 2, const formatter = new Intl.NumberFormat(locale, {
maximumFractionDigits: 2, style: "currency",
}); currency: "CAD",
return formatter.format(cents / 100); minimumFractionDigits: 2,
} catch (error) { maximumFractionDigits: 2,
// Fallback if locale is not supported });
const dollars = (cents / 100).toFixed(2); return formatter.format(cents / 100);
return `$${dollars}`; } catch {
} // Fallback if locale is not supported
const dollars = (cents / 100).toFixed(2);
return `$${dollars}`;
}
} }

View File

@@ -1,79 +1,79 @@
/** Custom error class for HTTP-related failures */ /** Custom error class for HTTP-related failures */
export class HttpError extends Error { export class HttpError extends Error {
constructor( constructor(
message: string, message: string,
public readonly statusCode: number, public readonly statusCode: number,
public readonly url?: string public readonly url?: string,
) { ) {
super(message); super(message);
this.name = "HttpError"; this.name = "HttpError";
} }
} }
/** Error class for network failures (timeouts, connection issues) */ /** Error class for network failures (timeouts, connection issues) */
export class NetworkError extends Error { export class NetworkError extends Error {
constructor( constructor(
message: string, message: string,
public readonly url: string, public readonly url: string,
public readonly cause?: Error public readonly cause?: Error,
) { ) {
super(message); super(message);
this.name = "NetworkError"; this.name = "NetworkError";
} }
} }
/** Error class for parsing failures */ /** Error class for parsing failures */
export class ParseError extends Error { export class ParseError extends Error {
constructor( constructor(
message: string, message: string,
public readonly data?: unknown public readonly data?: unknown,
) { ) {
super(message); super(message);
this.name = "ParseError"; this.name = "ParseError";
} }
} }
/** Error class for rate limiting */ /** Error class for rate limiting */
export class RateLimitError extends Error { export class RateLimitError extends Error {
constructor( constructor(
message: string, message: string,
public readonly url: string, public readonly url: string,
public readonly resetTime?: number public readonly resetTime?: number,
) { ) {
super(message); super(message);
this.name = "RateLimitError"; this.name = "RateLimitError";
} }
} }
/** Error class for validation failures */ /** Error class for validation failures */
export class ValidationError extends Error { export class ValidationError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);
this.name = "ValidationError"; this.name = "ValidationError";
} }
} }
/** Type guard to check if a value is a record (object) */ /** Type guard to check if a value is a record (object) */
export function isRecord(value: unknown): value is Record<string, unknown> { export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value); return typeof value === "object" && value !== null && !Array.isArray(value);
} }
/** /**
* Calculate exponential backoff delay with jitter * Calculate exponential backoff delay with jitter
*/ */
function calculateBackoffDelay(attempt: number, baseMs: number): number { function calculateBackoffDelay(attempt: number, baseMs: number): number {
const exponentialDelay = baseMs * 2 ** attempt; const exponentialDelay = baseMs * 2 ** attempt;
const jitter = Math.random() * 0.1 * exponentialDelay; // 10% jitter const jitter = Math.random() * 0.1 * exponentialDelay; // 10% jitter
return Math.min(exponentialDelay + jitter, 30000); // Cap at 30 seconds return Math.min(exponentialDelay + jitter, 30000); // Cap at 30 seconds
} }
/** Options for fetchHtml */ /** Options for fetchHtml */
export interface FetchHtmlOptions { export interface FetchHtmlOptions {
maxRetries?: number; maxRetries?: number;
retryBaseMs?: number; retryBaseMs?: number;
timeoutMs?: number; timeoutMs?: number;
onRateInfo?: (remaining: string | null, reset: string | null) => void; onRateInfo?: (remaining: string | null, reset: string | null) => void;
headers?: Record<string, string>; headers?: Record<string, string>;
} }
/** /**
@@ -85,116 +85,116 @@ export interface FetchHtmlOptions {
* @throws HttpError, NetworkError, or RateLimitError on failure * @throws HttpError, NetworkError, or RateLimitError on failure
*/ */
export async function fetchHtml( export async function fetchHtml(
url: string, url: string,
delayMs: number, delayMs: number,
opts?: FetchHtmlOptions opts?: FetchHtmlOptions,
): Promise<string> { ): Promise<string> {
const maxRetries = opts?.maxRetries ?? 3; const maxRetries = opts?.maxRetries ?? 3;
const retryBaseMs = opts?.retryBaseMs ?? 1000; const retryBaseMs = opts?.retryBaseMs ?? 1000;
const timeoutMs = opts?.timeoutMs ?? 30000; const timeoutMs = opts?.timeoutMs ?? 30000;
const defaultHeaders: Record<string, string> = { const defaultHeaders: Record<string, string> = {
accept: accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8", "accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
"cache-control": "no-cache", "cache-control": "no-cache",
"upgrade-insecure-requests": "1", "upgrade-insecure-requests": "1",
"user-agent": "user-agent":
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36",
}; };
for (let attempt = 0; attempt <= maxRetries; attempt++) { for (let attempt = 0; attempt <= maxRetries; attempt++) {
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs); const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const res = await fetch(url, { const res = await fetch(url, {
method: "GET", method: "GET",
headers: { ...defaultHeaders, ...opts?.headers }, headers: { ...defaultHeaders, ...opts?.headers },
signal: controller.signal, signal: controller.signal,
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);
const rateLimitRemaining = res.headers.get("X-RateLimit-Remaining"); const rateLimitRemaining = res.headers.get("X-RateLimit-Remaining");
const rateLimitReset = res.headers.get("X-RateLimit-Reset"); const rateLimitReset = res.headers.get("X-RateLimit-Reset");
opts?.onRateInfo?.(rateLimitRemaining, rateLimitReset); opts?.onRateInfo?.(rateLimitRemaining, rateLimitReset);
if (!res.ok) { if (!res.ok) {
// Handle rate limiting // Handle rate limiting
if (res.status === 429) { if (res.status === 429) {
const resetSeconds = rateLimitReset const resetSeconds = rateLimitReset
? Number(rateLimitReset) ? Number(rateLimitReset)
: Number.NaN; : Number.NaN;
const waitMs = Number.isFinite(resetSeconds) const waitMs = Number.isFinite(resetSeconds)
? Math.max(0, resetSeconds * 1000) ? Math.max(0, resetSeconds * 1000)
: calculateBackoffDelay(attempt, retryBaseMs); : calculateBackoffDelay(attempt, retryBaseMs);
if (attempt < maxRetries) { if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, waitMs)); await new Promise((resolve) => setTimeout(resolve, waitMs));
continue; continue;
} }
throw new RateLimitError( throw new RateLimitError(
`Rate limit exceeded for ${url}`, `Rate limit exceeded for ${url}`,
url, url,
resetSeconds resetSeconds,
); );
} }
// Retry on server errors // Retry on server errors
if (res.status >= 500 && res.status < 600 && attempt < maxRetries) { if (res.status >= 500 && res.status < 600 && attempt < maxRetries) {
await new Promise((resolve) => await new Promise((resolve) =>
setTimeout(resolve, calculateBackoffDelay(attempt, retryBaseMs)) setTimeout(resolve, calculateBackoffDelay(attempt, retryBaseMs)),
); );
continue; continue;
} }
throw new HttpError( throw new HttpError(
`Request failed with status ${res.status}`, `Request failed with status ${res.status}`,
res.status, res.status,
url url,
); );
} }
const html = await res.text(); const html = await res.text();
// Respect per-request delay to maintain rate limiting // Respect per-request delay to maintain rate limiting
await new Promise((resolve) => setTimeout(resolve, delayMs)); await new Promise((resolve) => setTimeout(resolve, delayMs));
return html; return html;
} catch (err) { } catch (err) {
// Re-throw known errors // Re-throw known errors
if ( if (
err instanceof RateLimitError || err instanceof RateLimitError ||
err instanceof HttpError || err instanceof HttpError ||
err instanceof NetworkError err instanceof NetworkError
) { ) {
throw err; throw err;
} }
if (err instanceof Error && err.name === "AbortError") { if (err instanceof Error && err.name === "AbortError") {
if (attempt < maxRetries) { if (attempt < maxRetries) {
await new Promise((resolve) => await new Promise((resolve) =>
setTimeout(resolve, calculateBackoffDelay(attempt, retryBaseMs)) setTimeout(resolve, calculateBackoffDelay(attempt, retryBaseMs)),
); );
continue; continue;
} }
throw new NetworkError(`Request timeout for ${url}`, url, err); throw new NetworkError(`Request timeout for ${url}`, url, err);
} }
// Network or other errors // Network or other errors
if (attempt < maxRetries) { if (attempt < maxRetries) {
await new Promise((resolve) => await new Promise((resolve) =>
setTimeout(resolve, calculateBackoffDelay(attempt, retryBaseMs)) setTimeout(resolve, calculateBackoffDelay(attempt, retryBaseMs)),
); );
continue; continue;
} }
throw new NetworkError( throw new NetworkError(
`Network error fetching ${url}: ${err instanceof Error ? err.message : String(err)}`, `Network error fetching ${url}: ${err instanceof Error ? err.message : String(err)}`,
url, url,
err instanceof Error ? err : undefined err instanceof Error ? err : undefined,
); );
} }
} }
throw new NetworkError(`Exhausted retries without response for ${url}`, url); throw new NetworkError(`Exhausted retries without response for ${url}`, url);
} }

View File

@@ -1,206 +1,219 @@
import { fetchKijijiItems, fetchFacebookItems, fetchEbayItems } from "@marketplace-scrapers/core"; import {
fetchEbayItems,
fetchFacebookItems,
fetchKijijiItems,
} from "@marketplace-scrapers/core";
import { tools } from "./tools"; import { tools } from "./tools";
/** /**
* Handle MCP JSON-RPC 2.0 protocol requests * Handle MCP JSON-RPC 2.0 protocol requests
*/ */
export async function handleMcpRequest(req: Request): Promise<Response> { export async function handleMcpRequest(req: Request): Promise<Response> {
try { try {
const body = await req.json(); const body = await req.json();
// Validate JSON-RPC 2.0 format // Validate JSON-RPC 2.0 format
if (!body.jsonrpc || body.jsonrpc !== "2.0" || !body.method) { if (!body.jsonrpc || body.jsonrpc !== "2.0" || !body.method) {
return Response.json( return Response.json(
{ {
jsonrpc: "2.0", jsonrpc: "2.0",
error: { code: -32600, message: "Invalid Request" }, error: { code: -32600, message: "Invalid Request" },
id: body.id, id: body.id,
}, },
{ status: 400 } { status: 400 },
); );
} }
const { method, params, id } = body; const { method, params, id } = body;
// Handle initialize method // Handle initialize method
if (method === "initialize") { if (method === "initialize") {
return Response.json({ return Response.json({
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
result: { result: {
protocolVersion: "2025-06-18", protocolVersion: "2025-06-18",
capabilities: { capabilities: {
tools: { tools: {
listChanged: true, listChanged: true,
}, },
}, },
serverInfo: { serverInfo: {
name: "marketplace-scrapers", name: "marketplace-scrapers",
version: "1.0.0", version: "1.0.0",
}, },
instructions: "Use search_kijiji, search_facebook, or search_ebay tools to find listings across Canadian marketplaces", instructions:
}, "Use search_kijiji, search_facebook, or search_ebay tools to find listings across Canadian marketplaces",
}); },
} });
}
// Handle tools/list method // Handle tools/list method
if (method === "tools/list") { if (method === "tools/list") {
return Response.json({ return Response.json({
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
result: { result: {
tools, tools,
}, },
}); });
} }
// Handle notifications (messages without id field should not get a response) // Handle notifications (messages without id field should not get a response)
if (!id) { if (!id) {
// Notifications don't require a response // Notifications don't require a response
if (method === "notifications/initialized") { if (method === "notifications/initialized") {
// Client initialized successfully, no response needed // Client initialized successfully, no response needed
return new Response(null, { status: 204 }); return new Response(null, { status: 204 });
} }
if (method === "notifications/progress") { if (method === "notifications/progress") {
// Progress notifications, no response needed // Progress notifications, no response needed
return new Response(null, { status: 204 }); return new Response(null, { status: 204 });
} }
// Unknown notification - still no response for notifications // Unknown notification - still no response for notifications
return new Response(null, { status: 204 }); return new Response(null, { status: 204 });
} }
// Handle tools/call method // Handle tools/call method
if (method === "tools/call") { if (method === "tools/call") {
const { name, arguments: args } = params || {}; const { name, arguments: args } = params || {};
if (!name || !args) { if (!name || !args) {
return Response.json( return Response.json(
{ {
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
error: { code: -32602, message: "Invalid params: name and arguments required" }, error: {
}, code: -32602,
{ status: 400 } message: "Invalid params: name and arguments required",
); },
} },
{ status: 400 },
);
}
// Route tool calls to appropriate handlers // Route tool calls to appropriate handlers
try { try {
let result; let result: unknown;
if (name === "search_kijiji") { if (name === "search_kijiji") {
const query = args.query; const query = args.query;
if (!query) { if (!query) {
return Response.json({ return Response.json({
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
error: { code: -32602, message: "query parameter is required" }, error: { code: -32602, message: "query parameter is required" },
}); });
} }
const searchOptions = { const searchOptions = {
location: args.location, location: args.location,
category: args.category, category: args.category,
keywords: args.keywords, keywords: args.keywords,
sortBy: args.sortBy, sortBy: args.sortBy,
sortOrder: args.sortOrder, sortOrder: args.sortOrder,
maxPages: args.maxPages || 5, maxPages: args.maxPages || 5,
priceMin: args.priceMin, priceMin: args.priceMin,
priceMax: args.priceMax, priceMax: args.priceMax,
}; };
const items = await fetchKijijiItems( const items = await fetchKijijiItems(
query, query,
1, 1,
"https://www.kijiji.ca", "https://www.kijiji.ca",
searchOptions, searchOptions,
{} {},
); );
result = items || []; result = items || [];
} else if (name === "search_facebook") { } else if (name === "search_facebook") {
const query = args.query; const query = args.query;
if (!query) { if (!query) {
return Response.json({ return Response.json({
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
error: { code: -32602, message: "query parameter is required" }, error: { code: -32602, message: "query parameter is required" },
}); });
} }
const items = await fetchFacebookItems( const items = await fetchFacebookItems(
query, query,
1, 1,
args.location || "toronto", args.location || "toronto",
args.maxItems || 25, args.maxItems || 25,
args.cookiesSource, args.cookiesSource,
undefined undefined,
); );
result = items || []; result = items || [];
} else if (name === "search_ebay") { } else if (name === "search_ebay") {
const query = args.query; const query = args.query;
if (!query) { if (!query) {
return Response.json({ return Response.json({
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
error: { code: -32602, message: "query parameter is required" }, error: { code: -32602, message: "query parameter is required" },
}); });
} }
const items = await fetchEbayItems(query, 1, { const items = await fetchEbayItems(query, 1, {
minPrice: args.minPrice, minPrice: args.minPrice,
maxPrice: args.maxPrice, maxPrice: args.maxPrice,
strictMode: args.strictMode || false, strictMode: args.strictMode || false,
exclusions: args.exclusions || [], exclusions: args.exclusions || [],
keywords: args.keywords || [query], keywords: args.keywords || [query],
buyItNowOnly: args.buyItNowOnly !== false, buyItNowOnly: args.buyItNowOnly !== false,
canadaOnly: args.canadaOnly !== false, canadaOnly: args.canadaOnly !== false,
}); });
const results = args.maxItems ? items.slice(0, args.maxItems) : items; const results = args.maxItems ? items.slice(0, args.maxItems) : items;
result = results || []; result = results || [];
} else { } else {
return Response.json({ return Response.json({
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
error: { code: -32601, message: `Unknown tool: ${name}` }, error: { code: -32601, message: `Unknown tool: ${name}` },
}); });
} }
return Response.json({ return Response.json({
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
result: { result: {
content: [ content: [
{ {
type: "text", type: "text",
text: JSON.stringify(result, null, 2), text: JSON.stringify(result, null, 2),
}, },
], ],
}, },
}); });
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"; const errorMessage =
return Response.json({ error instanceof Error ? error.message : "Unknown error";
jsonrpc: "2.0", return Response.json({
id, jsonrpc: "2.0",
error: { code: -32603, message: `Tool execution failed: ${errorMessage}` }, id,
}); error: {
} code: -32603,
} message: `Tool execution failed: ${errorMessage}`,
},
});
}
}
// Method not found // Method not found
return Response.json( return Response.json(
{ {
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
error: { code: -32601, message: `Method not found: ${method}` }, error: { code: -32601, message: `Method not found: ${method}` },
}, },
{ status: 404 } { status: 404 },
); );
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"; const errorMessage =
return Response.json( error instanceof Error ? error.message : "Unknown error";
{ return Response.json(
jsonrpc: "2.0", {
error: { code: -32700, message: `Parse error: ${errorMessage}` }, jsonrpc: "2.0",
}, error: { code: -32700, message: `Parse error: ${errorMessage}` },
{ status: 400 } },
); { status: 400 },
} );
}
} }

View File

@@ -3,23 +3,25 @@
*/ */
export const serverCard = { export const serverCard = {
$schema: "https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json", $schema:
version: "1.0", "https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json",
protocolVersion: "2025-06-18", version: "1.0",
serverInfo: { protocolVersion: "2025-06-18",
name: "marketplace-scrapers", serverInfo: {
title: "Marketplace Scrapers MCP Server", name: "marketplace-scrapers",
version: "1.0.0", title: "Marketplace Scrapers MCP Server",
}, version: "1.0.0",
transport: { },
type: "streamable-http", transport: {
endpoint: "/mcp", type: "streamable-http",
}, endpoint: "/mcp",
capabilities: { },
tools: { capabilities: {
listChanged: true, tools: {
}, listChanged: true,
}, },
description: "Scrapes marketplace listings from Kijiji, Facebook Marketplace, and eBay", },
tools: "dynamic", description:
"Scrapes marketplace listings from Kijiji, Facebook Marketplace, and eBay",
tools: "dynamic",
}; };

View File

@@ -3,135 +3,138 @@
*/ */
export const tools = [ export const tools = [
{ {
name: "search_kijiji", name: "search_kijiji",
description: "Search Kijiji marketplace for listings matching a query", description: "Search Kijiji marketplace for listings matching a query",
inputSchema: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
query: { query: {
type: "string", type: "string",
description: "Search query for Kijiji listings", description: "Search query for Kijiji listings",
}, },
location: { location: {
type: "string", type: "string",
description: "Location name or ID (e.g., 'toronto', 'gta', 'ontario')", description:
}, "Location name or ID (e.g., 'toronto', 'gta', 'ontario')",
category: { },
type: "string", category: {
description: "Category name or ID (e.g., 'computers', 'furniture', 'bikes')", type: "string",
}, description:
keywords: { "Category name or ID (e.g., 'computers', 'furniture', 'bikes')",
type: "string", },
description: "Additional keywords to filter results", keywords: {
}, type: "string",
sortBy: { description: "Additional keywords to filter results",
type: "string", },
description: "Sort results by field", sortBy: {
enum: ["relevancy", "date", "price", "distance"], type: "string",
default: "relevancy", description: "Sort results by field",
}, enum: ["relevancy", "date", "price", "distance"],
sortOrder: { default: "relevancy",
type: "string", },
description: "Sort order", sortOrder: {
enum: ["asc", "desc"], type: "string",
default: "desc", description: "Sort order",
}, enum: ["asc", "desc"],
maxPages: { default: "desc",
type: "number", },
description: "Maximum pages to fetch (~40 items per page)", maxPages: {
default: 5, type: "number",
}, description: "Maximum pages to fetch (~40 items per page)",
priceMin: { default: 5,
type: "number", },
description: "Minimum price in cents", priceMin: {
}, type: "number",
priceMax: { description: "Minimum price in cents",
type: "number", },
description: "Maximum price in cents", priceMax: {
}, type: "number",
}, description: "Maximum price in cents",
required: ["query"], },
}, },
}, required: ["query"],
{ },
name: "search_facebook", },
description: "Search Facebook Marketplace for listings matching a query", {
inputSchema: { name: "search_facebook",
type: "object", description: "Search Facebook Marketplace for listings matching a query",
properties: { inputSchema: {
query: { type: "object",
type: "string", properties: {
description: "Search query for Facebook Marketplace listings", query: {
}, type: "string",
location: { description: "Search query for Facebook Marketplace listings",
type: "string", },
description: "Location for search (e.g., 'toronto')", location: {
default: "toronto", type: "string",
}, description: "Location for search (e.g., 'toronto')",
maxItems: { default: "toronto",
type: "number", },
description: "Maximum number of items to return", maxItems: {
default: 5, type: "number",
}, description: "Maximum number of items to return",
cookiesSource: { default: 5,
type: "string", },
description: "Optional Facebook session cookies source", cookiesSource: {
}, type: "string",
}, description: "Optional Facebook session cookies source",
required: ["query"], },
}, },
}, required: ["query"],
{ },
name: "search_ebay", },
description: "Search eBay for listings matching a query (default: Buy It Now only, Canada only)", {
inputSchema: { name: "search_ebay",
type: "object", description:
properties: { "Search eBay for listings matching a query (default: Buy It Now only, Canada only)",
query: { inputSchema: {
type: "string", type: "object",
description: "Search query for eBay listings", properties: {
}, query: {
minPrice: { type: "string",
type: "number", description: "Search query for eBay listings",
description: "Minimum price filter", },
}, minPrice: {
maxPrice: { type: "number",
type: "number", description: "Minimum price filter",
description: "Maximum price filter", },
}, maxPrice: {
strictMode: { type: "number",
type: "boolean", description: "Maximum price filter",
description: "Enable strict search mode", },
default: false, strictMode: {
}, type: "boolean",
exclusions: { description: "Enable strict search mode",
type: "array", default: false,
items: { type: "string" }, },
description: "Terms to exclude from results", exclusions: {
}, type: "array",
keywords: { items: { type: "string" },
type: "array", description: "Terms to exclude from results",
items: { type: "string" }, },
description: "Keywords to include in search", keywords: {
}, type: "array",
buyItNowOnly: { items: { type: "string" },
type: "boolean", description: "Keywords to include in search",
description: "Include only Buy It Now listings (exclude auctions)", },
default: true, buyItNowOnly: {
}, type: "boolean",
canadaOnly: { description: "Include only Buy It Now listings (exclude auctions)",
type: "boolean", default: true,
description: "Include only Canadian sellers/listings", },
default: true, canadaOnly: {
}, type: "boolean",
maxItems: { description: "Include only Canadian sellers/listings",
type: "number", default: true,
description: "Maximum number of items to return", },
default: 5, maxItems: {
}, type: "number",
}, description: "Maximum number of items to return",
required: ["query"], default: 5,
}, },
}, },
required: ["query"],
},
},
]; ];