pinterest_search
Search for images on Pinterest using keywords to find visual content and inspiration for projects.
Instructions
Search for images on Pinterest by keyword
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| keyword | Yes | Search keyword | |
| limit | No | Number of images to return (default: 10) | |
| headless | No | Whether to use headless browser mode (default: true) |
Implementation Reference
- pinterest-mcp-server.ts:272-493 (handler)The MCP tool handler for 'pinterest_search'. Parses input arguments (handling various formats), calls PinterestScraper.search(), fixes thumbnail URLs to originals, formats results into MCP content array with text descriptions and links.private async handlePinterestSearch(args: any) { try { // Extract keyword and limits from input let keyword = ''; let limit = DEFAULT_SEARCH_LIMIT; let headless = DEFAULT_HEADLESS_MODE; // Normalize args if it's a string with backticks if (typeof args === 'string') { // Replace backticks with double quotes args = args.replace(/`/g, '"'); // console.error('Normalized args string:', args); } // Handle different input types if (args) { if (typeof args === 'object') { // If object, try to get properties directly // console.error('Args is object with keys:', Object.keys(args)); // Check for keyword property if ('keyword' in args && typeof args.keyword === 'string') { keyword = args.keyword.trim(); // console.error('Found keyword in object:', keyword); } else if ('`keyword`' in args) { keyword = String(args['`keyword`']).trim(); // console.error('Found `keyword` in object:', keyword); } // Check for limit property if ('limit' in args && (typeof args.limit === 'number' || !isNaN(parseInt(String(args.limit))))) { limit = typeof args.limit === 'number' ? args.limit : parseInt(String(args.limit), 10); // console.error('Found limit in object:', limit); } else if ('`limit`' in args) { const limitValue = args['`limit`']; limit = typeof limitValue === 'number' ? limitValue : parseInt(String(limitValue), 10); // console.error('Found `limit` in object:', limit); } // Check for headless property if ('headless' in args && typeof args.headless === 'boolean') { headless = args.headless; // console.error('Found headless in object:', headless); } else if ('`headless`' in args) { headless = Boolean(args['`headless`']); // console.error('Found `headless` in object:', headless); } } else if (typeof args === 'string') { // console.error('Args is string type, attempting to parse'); // Try to parse as JSON try { // First try standard JSON parsing let parsed; try { parsed = JSON.parse(args); // console.error('Successfully parsed as standard JSON'); } catch (jsonError) { // If that fails, try to fix common JSON format issues console.error('Standard JSON parse failed, trying to fix format'); // Replace single quotes with double quotes const fixedJson = args .replace(/'/g, '"') .replace(/(\w+):/g, '"$1":'); // Convert unquoted keys to quoted keys console.error('Attempting to parse fixed JSON:', fixedJson); parsed = JSON.parse(fixedJson); console.error('Successfully parsed fixed JSON'); } // Extract values from parsed object if (parsed) { if (parsed.keyword && typeof parsed.keyword === 'string') { keyword = parsed.keyword.trim(); // console.error('Found keyword in parsed JSON:', keyword); } if (parsed.limit !== undefined) { if (typeof parsed.limit === 'number') { limit = parsed.limit; } else if (typeof parsed.limit === 'string' && !isNaN(parseInt(parsed.limit))) { limit = parseInt(parsed.limit, 10); } // console.error('Found limit in parsed JSON:', limit); } if (parsed.headless !== undefined && typeof parsed.headless === 'boolean') { headless = parsed.headless; // console.error('Found headless in parsed JSON:', headless); } } } catch (e) { // console.error('All JSON parsing attempts failed, trying regex'); // If can't parse as JSON, try to extract using regex const keywordMatch = args.match(/["`']?keyword["`']?\s*[:=]\s*["`']([^"`']+)["`']/i); if (keywordMatch && keywordMatch[1]) { keyword = keywordMatch[1].trim(); // console.error('Found keyword using regex:', keyword); } // Try to extract limit const limitMatch = args.match(/["`']?limit["`']?\s*[:=]\s*(\d+)/i); if (limitMatch && limitMatch[1]) { limit = parseInt(limitMatch[1], 10); // console.error('Found limit using regex:', limit); } } } } // If keyword is empty, use default keyword if (!keyword) { keyword = DEFAULT_SEARCH_KEYWORD; // console.error('No keyword provided, using default keyword:', keyword); } // Ensure limit is a positive number if (isNaN(limit) || limit <= 0) { limit = DEFAULT_SEARCH_LIMIT; // console.error('Invalid limit, using default limit:', limit); } // console.error('Final parameters - keyword:', keyword, 'limit:', limit, 'headless:', headless); // Execute search let results = []; try { // 创建不会触发取消的AbortController const controller = new AbortController(); results = await this.scraper.search(keyword, limit, headless, controller.signal); } catch (searchError) { // console.error('Search error:', searchError); results = []; } // Ensure results is an array const validResults = Array.isArray(results) ? results : []; // Validate and fix image URLs for (const result of validResults) { if (result.image_url) { // Check if URL contains thumbnail markers const thumbnailPatterns = ['/60x60/', '/236x/', '/474x/', '/736x/']; let needsFix = false; // Check if matches any thumbnail pattern for (const pattern of thumbnailPatterns) { if (result.image_url.includes(pattern)) { needsFix = true; break; } } // Use regex to check more generic thumbnail formats if (!needsFix && result.image_url.match(/\/\d+x\d*\//)) { needsFix = true; } // If needs fixing, replace with original image URL if (needsFix) { // console.error(`Fixing thumbnail URL: ${result.image_url}`); result.image_url = result.image_url.replace(/\/\d+x\d*\//, '/originals/'); // console.error(`Fixed URL: ${result.image_url}`); } } } // Return results in MCP protocol format const contentItems = [ { type: 'text', text: `Found ${validResults.length} images related to "${keyword}" on Pinterest` } ]; // Add a text content item for each image result validResults.forEach((result, index) => { contentItems.push({ type: 'text', text: `Image ${index + 1}: ${result.title || 'No title'}` }); // Add image link contentItems.push({ type: 'text', text: `Link: ${result.image_url || 'No link'}` }); // Add original page link (if available) if (result.link && result.link !== result.image_url) { contentItems.push({ type: 'text', text: `Original page: ${result.link}` }); } // Add separator (except for last result) if (index < validResults.length - 1) { contentItems.push({ type: 'text', text: `---` }); } }); return { content: contentItems }; } catch (error: any) { console.error('Pinterest search handling error:', error); return { content: [ { type: 'text', text: `Error during search: ${error.message}` } ] }; } }
- pinterest-mcp-server.ts:177-196 (schema)Input schema defining parameters for pinterest_search: keyword (required), limit (optional), headless (optional).inputSchema: { type: 'object', properties: { keyword: { type: 'string', description: 'Search keyword', }, limit: { type: 'integer', description: `Number of images to return (default: ${DEFAULT_SEARCH_LIMIT})`, default: DEFAULT_SEARCH_LIMIT, }, headless: { type: 'boolean', description: `Whether to use headless browser mode (default: ${DEFAULT_HEADLESS_MODE})`, default: DEFAULT_HEADLESS_MODE, }, }, required: ['keyword'], },
- pinterest-mcp-server.ts:174-197 (registration)Tool registration in the MCP listTools response, including name, description, and input schema.{ name: 'pinterest_search', description: 'Search for images on Pinterest by keyword', inputSchema: { type: 'object', properties: { keyword: { type: 'string', description: 'Search keyword', }, limit: { type: 'integer', description: `Number of images to return (default: ${DEFAULT_SEARCH_LIMIT})`, default: DEFAULT_SEARCH_LIMIT, }, headless: { type: 'boolean', description: `Whether to use headless browser mode (default: ${DEFAULT_HEADLESS_MODE})`, default: DEFAULT_HEADLESS_MODE, }, }, required: ['keyword'], }, },
- pinterest-scraper.js:75-361 (helper)Core helper function implementing Pinterest image search using Puppeteer browser automation: launches Chrome, searches, auto-scrolls to load more, extracts image URLs/titles/links, converts thumbnails to original sizes, deduplicates and limits results.async search(keyword, limit = DEFAULT_SEARCH_LIMIT, headless = DEFAULT_HEADLESS_MODE, signal) { // Debug log for parameters // console.error('PinterestScraper.search called with:'); // console.error('- keyword:', keyword); // console.error('- limit:', limit); // console.error('- headless:', headless); // console.error('- signal:', signal ? 'provided' : 'not provided'); let browser = null; let page = null; try { // Support for cancellation if (signal && signal.aborted) { // console.error('Search aborted before starting'); throw new Error('操作被取消'); } // Build search URL const searchQuery = encodeURIComponent(keyword); const url = `${this.searchUrl}${searchQuery}`; // console.error('Search URL:', url); // Launch browser - using system installed Chrome try { // 在测试环境中使用 mock if (isTestEnvironment) { // console.error('Test environment detected, using mock browser'); // 在测试环境中,puppeteer-core已经被Jest模拟了,这里简单启动即可 // 不需要提供executablePath,因为模拟版本不会真正启动Chrome browser = await puppeteer.launch(); } else { const options = { executablePath: this.getChromePath(), headless: headless ? 'new' : false, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--disable-gpu', '--lang=zh-CN,zh' ] }; // 如果设置了代理服务器,添加到启动参数中 if (PROXY_SERVER) { console.log(`使用代理服务器: ${PROXY_SERVER}`); options.args.push(`--proxy-server=${PROXY_SERVER}`); } browser = await puppeteer.launch(options); } } catch (err) { // console.error('Failed to launch browser:', err.message); return []; } // Check for cancellation after browser launch if (signal && signal.aborted) { // console.error('Search aborted after browser launch'); await browser.close(); throw new Error('操作被取消'); } if (!browser) { // console.error('Browser is null, returning empty results'); return []; } // Create new page try { page = await browser.newPage(); } catch (err) { // console.error('Failed to create page:', err.message); await browser.close(); return []; } // Check for cancellation after page creation if (signal && signal.aborted) { // console.error('Search aborted after page creation'); await browser.close(); throw new Error('操作被取消'); } // Set viewport size await page.setViewport({ width: 1280, height: 800 }).catch(err => { // console.error('Failed to set viewport:', err.message); }); // Set user agent await page.setUserAgent('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').catch(err => { // console.error('Failed to set user agent:', err.message); }); // Set timeouts page.setDefaultNavigationTimeout(60000); page.setDefaultTimeout(30000); // 跟踪添加的事件监听器 const addedEventListeners = new Set(); // Simplify request interception try { await page.setRequestInterception(true); // Handle request interception with cancellation support const requestHandler = (req) => { // Check if operation was cancelled if (signal && signal.aborted) { req.abort(); return; } const resourceType = req.resourceType(); if (resourceType === 'image' || resourceType === 'font' || resourceType === 'media') { req.abort(); } else { req.continue(); } }; page.on('request', requestHandler); addedEventListeners.add('request'); } catch (err) { // console.error('Failed to set request interception:', err.message); } // Check for cancellation before navigation if (signal && signal.aborted) { // console.error('Search aborted before navigation'); await browser.close(); throw new Error('操作被取消'); } // Navigate to Pinterest search page try { await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 }); } catch (err) { // console.error('Page navigation failed:', err.message); await browser.close(); return []; } // Check for cancellation after navigation if (signal && signal.aborted) { // console.error('Search aborted after navigation'); await browser.close(); throw new Error('操作被取消'); } // Wait for images to load try { await page.waitForSelector('div[data-test-id="pin"]', { timeout: 10000 }); } catch (err) { // console.log('Pin elements not found, but continuing:', err.message); } // Check for cancellation before scrolling if (signal && signal.aborted) { // console.error('Search aborted before scrolling'); await browser.close(); throw new Error('操作被取消'); } // Scroll page to load more content try { // Calculate scroll distance based on limit const scrollDistance = Math.max(limit * 300, 1000); await this.autoScroll(page, scrollDistance, signal); } catch (err) { // If error is from cancellation, propagate it if (signal && signal.aborted) { // console.error('Scroll cancelled:', err.message); await browser.close(); throw new Error('操作被取消'); } // console.error('Failed to scroll page:', err.message); } // Check for cancellation before extracting images if (signal && signal.aborted) { // console.error('Search aborted before image extraction'); await browser.close(); throw new Error('操作被取消'); } // Extract image data let results = []; try { // Extract src attributes from all image elements results = await page.evaluate(() => { const images = Array.from(document.querySelectorAll('img')); return images .filter(img => img.src && img.src.includes('pinimg.com')) .map(img => { let imageUrl = img.src; // Handle various thumbnail sizes, convert to original size if (imageUrl.match(/\/\d+x\d*\//)) { imageUrl = imageUrl.replace(/\/\d+x\d*\//, '/originals/'); } // Replace specific thumbnail patterns const thumbnailPatterns = ['/60x60/', '/236x/', '/474x/', '/736x/']; for (const pattern of thumbnailPatterns) { if (imageUrl.includes(pattern)) { imageUrl = imageUrl.replace(pattern, '/originals/'); break; } } return { title: img.alt || 'Unknown Title', image_url: imageUrl, link: img.closest('a') ? img.closest('a').href : imageUrl, source: 'pinterest' }; }); }).catch(err => { // console.error('Failed to extract images:', err.message); return []; }); } catch (err) { // console.error('Error evaluating page:', err.message); results = []; } // Final cancellation check before processing results if (signal && signal.aborted) { // console.error('Search aborted before processing results'); await browser.close(); throw new Error('操作被取消'); } // Ensure results is an array const validResults = Array.isArray(results) ? results : []; // Deduplicate and limit results const uniqueResults = []; const urlSet = new Set(); for (const item of validResults) { if (uniqueResults.length >= limit) break; // Ensure item is valid object with image_url property if (item && typeof item === 'object' && item.image_url && !urlSet.has(item.image_url)) { urlSet.add(item.image_url); uniqueResults.push({ ...item, // Ensure 'source' field is present source: item.source || 'pinterest' }); } } return uniqueResults; } catch (error) { // Check if error is from cancellation if (signal && signal.aborted || error.message === '操作被取消') { // console.error('Pinterest search cancelled:', error.message); throw error; // Propagate cancellation error } // console.error('Pinterest search error:', error.message); return []; } finally { // 清理所有事件监听器 if (page) { try { page.removeAllListeners(); } catch (e) { // console.error('Error removing event listeners:', e.message); } } // Close browser if (browser) { try { await browser.close(); } catch (e) { // console.error('Error closing browser:', e.message); } } } }