Skip to main content
Glama

pinterest_search_and_download

Search Pinterest for images by keyword and download them directly to your device. Specify the number of images and use headless browser mode for automated retrieval.

Instructions

Search for images on Pinterest by keyword and download them

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
keywordYesSearch keyword
limitNoNumber of images to return and download (default: 10)
headlessNoWhether to use headless browser mode (default: true)

Implementation Reference

  • Main execution logic: parses args, searches Pinterest using scraper.search(), batch downloads images, formats MCP response with download results.
    private async handlePinterestSearchAndDownload(args: any) { try { // Extract parameters let keyword = ''; let limit = DEFAULT_SEARCH_LIMIT; let headless = DEFAULT_HEADLESS_MODE; let downloadDir = CURRENT_DOWNLOAD_DIR; // Normalize args if it's a string with backticks if (typeof args === 'string') { args = args.replace(/`/g, '"'); } // Handle different input types if (args) { if (typeof args === 'object') { // Extract keyword if ('keyword' in args && typeof args.keyword === 'string') { keyword = args.keyword.trim(); } else if ('`keyword`' in args) { keyword = String(args['`keyword`']).trim(); } // Extract limit 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); } else if ('`limit`' in args) { const limitValue = args['`limit`']; limit = typeof limitValue === 'number' ? limitValue : parseInt(String(limitValue), 10); } // Extract headless if ('headless' in args && typeof args.headless === 'boolean') { headless = args.headless; } else if ('`headless`' in args) { headless = Boolean(args['`headless`']); } } else if (typeof args === 'string') { // Try to parse as JSON try { let parsed; try { parsed = JSON.parse(args); } catch (jsonError) { const fixedJson = args .replace(/'/g, '"') .replace(/(\w+):/g, '"$1":'); parsed = JSON.parse(fixedJson); } if (parsed) { if (parsed.keyword && typeof parsed.keyword === 'string') { keyword = parsed.keyword.trim(); } 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); } } if (parsed.headless !== undefined && typeof parsed.headless === 'boolean') { headless = parsed.headless; } } } catch (e) { // 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(); } // Try to extract limit const limitMatch = args.match(/["`']?limit["`']?\s*[:=]\s*(\d+)/i); if (limitMatch && limitMatch[1]) { limit = parseInt(limitMatch[1], 10); } } } } // Use defaults if values are missing if (!keyword) { keyword = DEFAULT_SEARCH_KEYWORD; } if (isNaN(limit) || limit <= 0) { limit = DEFAULT_SEARCH_LIMIT; } // 始终使用环境变量设置或默认值 downloadDir = CURRENT_DOWNLOAD_DIR; // 创建包含关键词的下载目录 const keywordDir = path.join(downloadDir, keyword.replace(/\s+/g, '_').replace(/[^a-zA-Z0-9_\u4e00-\u9fa5]/g, '')); // Create download directory if it doesn't exist try { if (!fs.existsSync(keywordDir)) { fs.mkdirSync(keywordDir, { recursive: true }); } } catch (dirError: any) { return { content: [ { type: 'text', text: `创建下载目录失败: ${dirError.message}` } ] }; } // Execute search let results = []; try { // 创建不会触发取消的AbortController const controller = new AbortController(); results = await this.scraper.search(keyword, limit, headless, controller.signal); } catch (searchError) { results = []; } // Ensure results is an array const validResults = Array.isArray(results) ? results : []; // 最大重试次数,提高稳定性 const maxRetries = 3; // 使用batchDownload函数进行批量下载,传入文件名模板 const downloadResult = await batchDownload(validResults, keywordDir, { filenameTemplate: CURRENT_FILENAME_TEMPLATE, maxRetries }) as DownloadResult; // Return results in MCP protocol format const contentItems = [ { type: 'text', text: `搜索并下载了 ${validResults.length} 张与"${keyword}"相关的图片` }, { type: 'text', text: `成功: ${downloadResult.downloadedCount}, 失败: ${downloadResult.failedCount}` } ]; // Add a text content item for each download result downloadResult.downloaded.forEach((result: {success: boolean; id: string; path: string; url: string}, index: number) => { contentItems.push({ type: 'text', text: `图片 ${index + 1}: ${validResults[index]?.title || 'Unknown Title'}` }); contentItems.push({ type: 'text', text: `链接: ${result.url}` }); contentItems.push({ type: 'text', text: `保存位置: ${result.path}` }); // Add separator (except for last result) if (index < downloadResult.downloaded.length - 1) { contentItems.push({ type: 'text', text: `---` }); } }); // Add failed downloads if any if (downloadResult.failedCount > 0) { contentItems.push({ type: 'text', text: `--- 下载失败的图片 ---` }); downloadResult.failed.forEach((failed: {url: string; error: string}, index: number) => { contentItems.push({ type: 'text', text: `失败 ${index + 1}: ${failed.url}` }); contentItems.push({ type: 'text', text: `错误: ${failed.error}` }); // Add separator (except for last result) if (index < downloadResult.failed.length - 1) { contentItems.push({ type: 'text', text: `---` }); } }); } return { content: contentItems }; } catch (error: any) { console.error('Pinterest search and download handling error:', error); return { content: [ { type: 'text', text: `搜索和下载过程中出错: ${error.message}` } ] }; } }
  • Tool registration in ListToolsRequestSchema handler, including name, description, and input schema.
    { name: 'pinterest_search_and_download', description: 'Search for images on Pinterest by keyword and download them', inputSchema: { type: 'object', properties: { keyword: { type: 'string', description: 'Search keyword', }, limit: { type: 'integer', description: `Number of images to return and download (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'], }, }
  • Switch case dispatcher in CallToolRequestSchema handler that routes to the tool handler.
    case 'pinterest_search_and_download': return await this.handlePinterestSearchAndDownload(request.params.args || request.params.arguments);
  • PinterestScraper.search(): Core helper that scrapes images from Pinterest search page using Puppeteer.
    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); } } } }
  • batchDownload(): Helper utility that downloads multiple images with retry logic and filename templating.
    export async function batchDownload(results, downloadDir, options = {}) { const { filenameTemplate = DEFAULT_FILENAME_TEMPLATE, maxRetries = 3 } = options; // 确保下载目录存在 if (!fs.existsSync(downloadDir)) { await fs.promises.mkdir(downloadDir, { recursive: true }); } const downloadResults = { success: true, total: results.length, downloadedCount: 0, failedCount: 0, downloaded: [], failed: [] }; // 批量下载图片 for (let i = 0; i < results.length; i++) { const result = results[i]; try { if (result?.image_url) { const downloadResult = await downloadImage(result, downloadDir, { filenameTemplate, maxRetries, index: i + 1 }); downloadResults.downloaded.push(downloadResult); downloadResults.downloadedCount++; } } catch (error) { downloadResults.failed.push({ url: result?.image_url ?? 'unknown', error: error.message }); downloadResults.failedCount++; } } return downloadResults; }

Latest Blog Posts

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/terryso/mcp-pinterest'

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