Skip to main content
Glama

pinterest_search

Retrieve Pinterest images by keyword using the MCP server. Specify search terms, result limit, and browser mode for targeted image searches.

Instructions

Search for images on Pinterest by keyword

Input Schema

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

Implementation Reference

  • Registration of the pinterest_search tool in the MCP ListTools handler, defining 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'], }, },
  • MCP tool handler for pinterest_search: parses input arguments, calls PinterestScraper.search, processes results (including URL fixes), and returns formatted MCP content response.
    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}` } ] }; } }
  • Core helper function PinterestScraper.search(): implements the web scraping logic using Puppeteer-core to search Pinterest, scroll for more content, extract and process image URLs.
    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); } } } }

Other Tools

Related Tools

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