Skip to main content
Glama

Begagnad MCP

by bjesus
index.ts13.3 kB
import { McpAgent } from "agents/mcp"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { parseStringPromise } from "xml2js"; // Environment interface interface Env { TRADERA_APP_ID: string; TRADERA_APP_KEY: string; } // Unified item interface for both marketplaces interface UnifiedItem { id: string; title: string; description: string; price: number | null; currency: string; location: string; url: string; images: string[]; condition: string | null; seller: { name: string; rating: number | null; }; endDate: string | null; source: "blocket" | "tradera"; itemType?: string; } // Blocket API functions async function searchBlocket(query: string, limit: number = 20): Promise<UnifiedItem[]> { const url = `https://blocket-api.se/v1/custom-search?query=${encodeURIComponent(query)}&limit=${limit}`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`Blocket API error: ${response.status} ${response.statusText}`); } const data = await response.json() as any; if (!data.data || !Array.isArray(data.data)) { return []; } return data.data.map((item: any) => ({ id: item.ad_id || item.list_id, title: item.subject || "", description: item.body || "", price: item.price?.value || null, currency: "SEK", location: item.location?.[0]?.name || item.map?.label || "", url: item.share_url || "", images: (item.images || []).map((img: any) => img.url).filter(Boolean), condition: item.parameter_groups?.find((g: any) => g.type === "general") ?.parameters?.find((p: any) => p.id === "item_condition")?.short_value || null, seller: { name: item.advertiser?.name || "", rating: item.advertiser?.public_profile?.reviews?.overall_score || null, }, endDate: item.list_time || null, source: "blocket" as const, })); } catch (error) { throw new Error(`Failed to search Blocket: ${error instanceof Error ? error.message : String(error)}`); } } async function getBlocketItem(adId: string): Promise<UnifiedItem> { const url = `https://blocket-api.se/v1/get-ad-by-id?ad_id=${adId}`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`Blocket API error: ${response.status} ${response.statusText}`); } const result = await response.json() as any; const item = result.data; if (!item) { throw new Error(`Item not found: ${adId}`); } return { id: item.ad_id || item.list_id, title: item.subject || "", description: item.body || "", price: item.price?.value || null, currency: "SEK", location: item.location?.[0]?.name || item.map?.label || "", url: item.share_url || "", images: (item.images || []).map((img: any) => img.url).filter(Boolean), condition: item.parameter_groups?.find((g: any) => g.type === "general") ?.parameters?.find((p: any) => p.id === "item_condition")?.short_value || null, seller: { name: item.advertiser?.name || "", rating: item.advertiser?.public_profile?.reviews?.overall_score || null, }, endDate: item.list_time || null, source: "blocket" as const, }; } catch (error) { throw new Error(`Failed to get Blocket item: ${error instanceof Error ? error.message : String(error)}`); } } // Tradera API functions async function searchTradera(query: string, env: Env, pageNumber: number = 1): Promise<UnifiedItem[]> { const url = `https://api.tradera.com/v3/searchservice.asmx/Search?query=${encodeURIComponent(query)}&categoryId=0&pageNumber=${pageNumber}&orderBy=Relevance&appId=${env.TRADERA_APP_ID}&appKey=${env.TRADERA_APP_KEY}`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`Tradera API error: ${response.status} ${response.statusText}`); } const xmlText = await response.text(); const result = await parseStringPromise(xmlText); const items = result?.SearchResult?.Items || []; return items.map((item: any) => { const imageLinks = item.ImageLinks?.[0]?.ImageLink || []; const images = imageLinks .filter((link: any) => link.Format?.[0] === "normal") .map((link: any) => link.Url?.[0]) .filter(Boolean); const condition = item.AttributeValues?.[0]?.TermAttributeValues?.[0]?.TermAttributeValue ?.find((attr: any) => attr.Name?.[0] === "condition") ?.Values?.[0]?.string?.[0] || null; return { id: item.Id?.[0] || "", title: item.ShortDescription?.[0] || "", description: item.LongDescription?.[0] || "", price: item.BuyItNowPrice?.[0] || item.MaxBid?.[0] || item.NextBid?.[0] || null, currency: "SEK", location: "", url: item.ItemUrl?.[0] || "", images, condition, seller: { name: item.SellerAlias?.[0] || "", rating: item.SellerDsrAverage?.[0] ? parseFloat(item.SellerDsrAverage[0]) : null, }, endDate: item.EndDate?.[0] || null, source: "tradera" as const, itemType: item.ItemType?.[0] || "", }; }); } catch (error) { throw new Error(`Failed to search Tradera: ${error instanceof Error ? error.message : String(error)}`); } } async function getTraderaItem(itemId: string, env: Env): Promise<UnifiedItem> { const url = `https://api.tradera.com/v3/publicservice.asmx/GetItem?itemId=${itemId}&appId=${env.TRADERA_APP_ID}&appKey=${env.TRADERA_APP_KEY}`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`Tradera API error: ${response.status} ${response.statusText}`); } const xmlText = await response.text(); const result = await parseStringPromise(xmlText); const item = result?.Item; if (!item) { throw new Error(`Item not found: ${itemId}`); } const imageLinks = item.DetailedImageLinks || []; const images = imageLinks .filter((link: any) => link.Format?.[0] === "normal") .map((link: any) => link.Url?.[0]) .filter(Boolean); const condition = item.AttributeValues?.[0]?.TermAttributeValues?.[0]?.TermAttributeValue ?.find((attr: any) => attr.Id?.[0] === "121") ?.Values?.[0]?.string?.[0] || null; return { id: item.Id?.[0] || "", title: item.ShortDescription?.[0] || "", description: item.LongDescription?.[0]?.replace(/<br>/g, "\n").replace(/&lt;/g, "<").replace(/&gt;/g, ">") || "", price: item.MaxBid?.[0] || item.NextBid?.[0] || null, currency: "SEK", location: item.Seller?.[0]?.City?.[0] || "", url: item.ItemLink?.[0] || "", images, condition, seller: { name: item.Seller?.[0]?.Alias?.[0] || "", rating: item.Seller?.[0]?.TotalRating?.[0] ? parseFloat(item.Seller[0].TotalRating[0]) : null, }, endDate: item.EndDate?.[0] || null, source: "tradera" as const, itemType: item.ItemType?.[0] || "", }; } catch (error) { throw new Error(`Failed to get Tradera item: ${error instanceof Error ? error.message : String(error)}`); } } // Define our MCP agent with tools export class BegagnadMCP extends McpAgent { server = new McpServer({ name: "Begagnad - Swedish Second-Hand Marketplace Search", version: "1.0.0", }); // env is inherited from McpAgent base class async init() { // Search Blocket this.server.tool( "search_blocket", { query: z.string().describe("Search query (e.g., 'Linksys router OpenWRT', 'red pickup truck')"), limit: z.number().optional().default(20).describe("Maximum number of results (default: 20, max: 99)"), }, async ({ query, limit }) => { try { const items = await searchBlocket(query, limit); return { content: [ { type: "text", text: JSON.stringify({ count: items.length, items }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } ); // Get Blocket item this.server.tool( "get_blocket_item", { ad_id: z.string().describe("The Blocket ad ID"), }, async ({ ad_id }) => { try { const item = await getBlocketItem(ad_id); return { content: [ { type: "text", text: JSON.stringify(item, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } ); // Search Tradera this.server.tool( "search_tradera", { query: z.string().describe("Search query (e.g., 'Linksys router OpenWRT', 'red pickup truck')"), page: z.number().optional().default(1).describe("Page number for pagination (default: 1)"), }, async ({ query, page }) => { try { const items = await searchTradera(query, this.env as Env, page); return { content: [ { type: "text", text: JSON.stringify({ count: items.length, items }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } ); // Get Tradera item this.server.tool( "get_tradera_item", { item_id: z.string().describe("The Tradera item ID"), }, async ({ item_id }) => { try { const item = await getTraderaItem(item_id, this.env as Env); return { content: [ { type: "text", text: JSON.stringify(item, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } ); // Search both marketplaces this.server.tool( "search_both", { query: z.string().describe("Search query (e.g., 'Linksys router OpenWRT', 'red pickup truck')"), blocket_limit: z.number().optional().default(20).describe("Maximum number of Blocket results (default: 20)"), }, async ({ query, blocket_limit }) => { try { const [blocketItems, traderaItems] = await Promise.all([ searchBlocket(query, blocket_limit), searchTradera(query, this.env as Env, 1), ]); const allItems = [...blocketItems, ...traderaItems]; return { content: [ { type: "text", text: JSON.stringify({ total: allItems.length, blocket_count: blocketItems.length, tradera_count: traderaItems.length, items: allItems, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } ); } } export default { fetch(request: Request, env: Env, ctx: ExecutionContext) { const url = new URL(request.url); if (url.pathname === "/sse" || url.pathname === "/sse/message") { return BegagnadMCP.serveSSE("/sse").fetch(request, env, ctx); } if (url.pathname === "/mcp") { return BegagnadMCP.serve("/mcp").fetch(request, env, ctx); } // Root path - show info if (url.pathname === "/") { return new Response( JSON.stringify({ name: "Begagnad MCP Server", description: "Search Swedish second-hand marketplaces (Blocket & Tradera)", endpoints: { sse: "/sse", mcp: "/mcp", }, tools: [ "search_blocket", "get_blocket_item", "search_tradera", "get_tradera_item", "search_both", ], }, null, 2), { headers: { "Content-Type": "application/json" }, } ); } return new Response("Not found", { status: 404 }); }, };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bjesus/begagnad-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server