Skip to main content
Glama

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
NameRequiredDescriptionDefault
keywordYesSearch keyword
limitNoNumber of images to return (default: 10)
headlessNoWhether to use headless browser mode (default: true)

Implementation Reference

  • 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}`
            }
          ]
        };
      }
    }
  • 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'],
    },
  • 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'],
      },
    },
  • 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);
          }
        }
      }
    }

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