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