From 05f74f99efe02d20302bf823ec22867a5720870f Mon Sep 17 00:00:00 2001 From: orangecoding Date: Thu, 9 Apr 2026 11:51:42 +0200 Subject: [PATCH] adding tool to receive photo of listing --- lib/mcp/mcpAdapter.js | 116 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/lib/mcp/mcpAdapter.js b/lib/mcp/mcpAdapter.js index 9d9601e..7ef6531 100644 --- a/lib/mcp/mcpAdapter.js +++ b/lib/mcp/mcpAdapter.js @@ -220,6 +220,122 @@ export function createMcpServer() { }, ); + // ── get_photo_for_listing ───────────────────────────────────────────────────── + server.tool( + 'get_photo_for_listing', + 'Fetch and return the photo of a listing by its ID as an image for vision analysis.', + { + listingId: z.string().describe('The listing ID whose photo to fetch'), + }, + async ({ listingId }, extra) => { + const { user, error } = authenticateToolCall(extra, 'get_photo_for_listing'); + if (error) return normalizeError(error, 'get_photo_for_listing'); + + const listing = getListingById(listingId, user.id, user.isAdmin); + if (!listing) { + return normalizeError('Listing not found or access denied.', 'get_photo_for_listing'); + } + + const imageUrl = listing.image_url; + if (!imageUrl) { + return normalizeError('No image available for this listing.', 'get_photo_for_listing'); + } + + const SUPPORTED_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp']); + + let response; + try { + response = await fetch(imageUrl, { + signal: AbortSignal.timeout(10_000), + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Accept: 'image/jpeg,image/png,image/webp,image/gif,image/*,*/*', + }, + }); + } catch (fetchErr) { + return normalizeError(`Failed to fetch image: ${fetchErr.message}`, 'get_photo_for_listing'); + } + + if (!response.ok) { + return normalizeError( + `Image fetch returned HTTP ${response.status}. Image URL: ${imageUrl}`, + 'get_photo_for_listing', + ); + } + + const contentType = response.headers.get('content-type') ?? ''; + const headerMimeType = contentType.split(';')[0].trim().toLowerCase(); + + let buffer; + try { + buffer = await response.arrayBuffer(); + } catch (readErr) { + return normalizeError(`Failed to read image body: ${readErr.message}`, 'get_photo_for_listing'); + } + + const bytes = new Uint8Array(buffer); + + if (bytes.length < 12) { + return normalizeError( + `Downloaded file is too small to determine image type. Image URL: ${imageUrl}`, + 'get_photo_for_listing', + ); + } + + let resolvedMime; + + if (SUPPORTED_MIME_TYPES.has(headerMimeType)) { + resolvedMime = headerMimeType; + } else { + if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) { + resolvedMime = 'image/jpeg'; + } else if ( + bytes[0] === 0x89 && + bytes[1] === 0x50 && + bytes[2] === 0x4e && + bytes[3] === 0x47 && + bytes[4] === 0x0d && + bytes[5] === 0x0a && + bytes[6] === 0x1a && + bytes[7] === 0x0a + ) { + resolvedMime = 'image/png'; + } else if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38) { + resolvedMime = 'image/gif'; + } else if ( + bytes[0] === 0x52 && + bytes[1] === 0x49 && + bytes[2] === 0x46 && + bytes[3] === 0x46 && + bytes[8] === 0x57 && + bytes[9] === 0x45 && + bytes[10] === 0x42 && + bytes[11] === 0x50 + ) { + resolvedMime = 'image/webp'; + } else { + return normalizeError( + `Image format not supported by vision models (header: ${headerMimeType || 'unknown'}). Image URL: ${imageUrl}`, + 'get_photo_for_listing', + ); + } + } + + const base64 = Buffer.from(buffer).toString('base64'); + + return { + content: [ + { + type: 'image', + data: base64, + mimeType: resolvedMime, + }, + ], + }; + }, + ); + // ── get_current_date_ime ───────────────────────────────────────────────────── server.tool('get_current_date_time', 'Returns the current date and time.', {}, () => { return {