UNPKG

rapidapi-mcp-server

Version:

MCP server for discovering and assessing APIs from RapidAPI marketplace

309 lines (308 loc) 16.6 kB
import { BrowserManager } from './browser-manager.js'; import { SearchResultsExtractor, APIDetailExtractor } from './extractors.js'; export class RapidAPIClient { browserManager; searchExtractor; detailExtractor; constructor() { // Use headless configuration for server environment this.browserManager = new BrowserManager({ headless: true, timeout: 60000 // Increase timeout for better reliability }); this.searchExtractor = new SearchResultsExtractor(); this.detailExtractor = new APIDetailExtractor(); } async searchAPIs(options) { const page = await this.browserManager.createPage(); try { console.error(`Searching for APIs with query: ${options.query} using natural workflow`); // Navigate directly to hub page (exactly like working Puppeteer MCP test) await this.browserManager.navigateWithRetry(page, 'https://rapidapi.com/hub'); // NO handleAntiBot call here - only do it if we detect issues later // Use natural search workflow (Ctrl+K method validated by QA) console.error('Activating search with Ctrl+K shortcut'); await page.keyboard.down('Control'); await page.keyboard.press('KeyK'); await page.keyboard.up('Control'); // Wait for search modal to appear and type query await page.waitForSelector('input[placeholder="Search..."]', { timeout: 10000 }); await page.type('input[placeholder="Search..."]', options.query); // Execute search with Enter key await page.keyboard.press('Enter'); // Wait for search results to load await this.browserManager.waitForNetworkIdle(page); // Extract search results from the results page const results = await this.searchExtractor.extractSearchResults(page, options.maxResults || 20); console.error(`Found ${results.length} API results using natural workflow`); return results; } catch (error) { console.error('Error during natural search workflow:', error); throw new Error(`Failed to search APIs using natural workflow: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { await page.close(); } } async assessAPI(apiUrl) { const page = await this.browserManager.createPage(); try { console.error(`Assessing API at: ${apiUrl}`); // Navigate to API detail page await this.browserManager.navigateWithRetry(page, apiUrl); // Handle anti-bot measures await this.browserManager.handleAntiBot(page); // Wait for page content to load await this.browserManager.waitForNetworkIdle(page); // Extract comprehensive API details const assessment = await this.detailExtractor.extractAPIDetails(page); console.error(`Successfully assessed API: ${assessment.name}`); return assessment; } catch (error) { console.error('Error during API assessment:', error); throw new Error(`Failed to assess API: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { await page.close(); } } async getPricingPlans(apiUrl) { const page = await this.browserManager.createPage(); try { console.error(`Extracting pricing plans for API: ${apiUrl}`); // Navigate to the main API page (not playground) const mainApiUrl = apiUrl.replace(/\/playground.*$/, ''); await this.browserManager.navigateWithRetry(page, mainApiUrl); // Handle anti-bot measures await this.browserManager.handleAntiBot(page); // Look for subscription plans link and click it const subscriptionSelector = '.inline-flex.items-center.justify-center.gap-1.text-xs.font-normal'; try { await page.waitForSelector(subscriptionSelector, { timeout: 5000 }); await page.click(subscriptionSelector); await this.browserManager.waitForNetworkIdle(page); } catch (e) { // If no subscription link found, try to extract basic pricing from current page console.error('No subscription plans link found, extracting basic pricing'); } // Extract pricing information from the page const pricingData = await page.evaluate(() => { const pricing = {}; // Look for pricing plan containers const planElements = Array.from(document.querySelectorAll('[class*="plan"], [class*="pricing"], div:has(> h3:contains("Ultra")), div:has(> h3:contains("Mega"))')); planElements.forEach(element => { const planText = element.textContent || ''; // Extract Ultra plan if (planText.includes('Ultra') && planText.includes('$')) { const priceMatch = planText.match(/\$([0-9,]+\.?\d*)/); const billingMatch = planText.match(/\/\s*(mo|month|year)/); pricing.ultra = { name: 'Ultra', price: priceMatch ? `$${priceMatch[1]}` : 'Unknown', billing: billingMatch ? `/${billingMatch[1]}` : '/mo', recommended: planText.includes('Recommended'), features: {}, capabilities: [], restrictions: [] }; // Extract feature limits const dayLimits = planText.match(/([0-9,]+)\s*\/\s*Day/g); if (dayLimits) { dayLimits.forEach(limit => { const number = limit.match(/([0-9,]+)/)?.[1]; if (planText.includes('domain search') && number) { pricing.ultra.features['Commercial full domain search'] = `${number} / Day`; } if (planText.includes('Leaked DB') && number) { pricing.ultra.features['Source Search over 1300+ Leaked DBs'] = `${number} / Day`; } }); } // Extract rate limit if (planText.includes('1 requests per second')) { pricing.ultra.rateLimit = '1 requests per second'; } } // Extract Mega plan if (planText.includes('Mega') && planText.includes('$')) { const priceMatch = planText.match(/\$([0-9,]+\.?\d*)/); const billingMatch = planText.match(/\/\s*(mo|month|year)/); pricing.mega = { name: 'Mega', price: priceMatch ? `$${priceMatch[1]}` : 'Unknown', billing: billingMatch ? `/${billingMatch[1]}` : '/mo', recommended: false, features: {}, capabilities: [], restrictions: [] }; // Extract mega plan limits const dayLimits = planText.match(/([0-9,]+)\s*\/\s*Day/g); if (dayLimits) { dayLimits.forEach(limit => { const number = limit.match(/([0-9,]+)/)?.[1]; if (planText.includes('domain search') && number) { pricing.mega.features['Commercial full domain search'] = `${number} / Day`; } if (planText.includes('Leaked DB') && number) { pricing.mega.features['Source Search over 1300+ Leaked DBs'] = `${number} / Day`; } }); } // Extract rate limit if (planText.includes('5 requests per second')) { pricing.mega.rateLimit = '5 requests per second'; } } }); return pricing; }); console.error(`Extracted pricing for ${Object.keys(pricingData).length} plans`); return pricingData; } catch (error) { console.error('Error extracting pricing plans:', error); throw new Error(`Failed to extract pricing plans: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { await page.close(); } } async getAPIDocumentation(apiUrl) { const page = await this.browserManager.createPage(); try { console.error(`Getting comprehensive documentation for API at: ${apiUrl}`); // Set up GraphQL request monitoring const graphqlRequests = []; page.on('response', async (response) => { const url = response.url(); if (url.includes('rapidapi.com/gateway/graphql') && response.request().method() === 'POST') { try { const responseData = await response.json(); graphqlRequests.push({ url: url, data: responseData, timestamp: new Date().toISOString() }); console.error(`Captured GraphQL response with operation data`); } catch (e) { console.error('Failed to parse GraphQL response:', e); } } }); // Navigate to the API playground page for endpoint details const playgroundUrl = apiUrl.includes('/playground') ? apiUrl : `${apiUrl}/playground`; await this.browserManager.navigateWithRetry(page, playgroundUrl); await this.browserManager.handleAntiBot(page); await this.browserManager.waitForNetworkIdle(page); // Extract endpoint information from sidebar const sidebarEndpoints = await page.evaluate(() => { const endpointLinks = Array.from(document.querySelectorAll('a[href*="/playground/apiendpoint"]')); return endpointLinks.map(link => { const href = link.href; const endpointId = href.match(/apiendpoint_([a-f0-9-]+)/)?.[1]; // Extract method and name from the link structure const methodElement = link.querySelector('[class*="text-blue-500"], [class*="text-green-500"]'); const nameElement = link.querySelector('[class*="text-xs"]'); return { id: endpointId, name: nameElement?.textContent?.trim() || 'Unknown', method: methodElement?.textContent?.trim() || 'GET', url: href, playgroundUrl: href }; }); }); // Click on endpoint links to trigger GraphQL requests for detailed info for (const endpoint of sidebarEndpoints.slice(0, 3)) { // Limit to first 3 for ethical usage try { console.error(`Fetching details for endpoint: ${endpoint.name}`); await page.click(`a[href="${endpoint.url}"]`); await this.browserManager.waitForNetworkIdle(page); // Apply ethical rate limiting between endpoint requests const { ETHICAL_RATE_LIMITS } = await import('./rate-limiting-config.js'); const delay = Math.random() * 2000 + 1000; // 1-3 second delay await new Promise(resolve => setTimeout(resolve, delay)); } catch (e) { console.error(`Failed to fetch details for ${endpoint.name}:`, e); } } // Extract comprehensive endpoint documentation const docInfo = await page.evaluate(() => { // Look for documentation links const docLinks = Array.from(document.querySelectorAll('a[href*="doc"], a[href*="documentation"]')); const documentationUrl = docLinks.length > 0 ? docLinks[0].href : undefined; // Get all endpoint links with enhanced information const endpointElements = Array.from(document.querySelectorAll('a[href*="/playground/apiendpoint"]')); const endpoints = endpointElements.map(el => { const href = el.href; const endpointId = href.match(/apiendpoint_([a-f0-9-]+)/)?.[1]; // Extract method color coding const methodElement = el.querySelector('[class*="text-blue-500"], [class*="text-green-500"], [class*="text-red-500"]'); const nameElement = el.querySelector('[class*="text-xs"]:not([class*="text-blue"]):not([class*="text-green"])'); let method = 'GET'; if (methodElement) { const methodText = methodElement.textContent?.trim().toUpperCase(); if (methodText && ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(methodText)) { method = methodText; } } return { id: endpointId, name: nameElement?.textContent?.trim() || 'Unknown Endpoint', method: method, url: href, description: `${method} endpoint for ${nameElement?.textContent?.trim() || 'API operation'}`, route: undefined, parameters: [] }; }).filter(ep => ep.name && ep.name !== 'Unknown Endpoint'); return { documentationUrl, endpoints }; }); // Merge GraphQL data if available if (graphqlRequests.length > 0) { console.error(`Enhanced documentation with ${graphqlRequests.length} GraphQL responses`); // Add GraphQL-sourced details to endpoints where available docInfo.endpoints.forEach(endpoint => { const matchingRequest = graphqlRequests.find(req => req.data?.data?.endpoint?.id === endpoint.id); if (matchingRequest?.data?.data?.endpoint) { const graphqlEndpoint = matchingRequest.data.data.endpoint; endpoint.description = graphqlEndpoint.description || endpoint.description; endpoint.route = graphqlEndpoint.route; endpoint.parameters = graphqlEndpoint.params?.parameters || []; } }); } console.error(`Extracted ${docInfo.endpoints.length} endpoints with detailed information`); return docInfo; } catch (error) { console.error('Error getting API documentation:', error); throw new Error(`Failed to get documentation: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { await page.close(); } } async close() { await this.browserManager.closeBrowser(); } buildSearchUrl(options) { const baseUrl = 'https://rapidapi.com/search'; const params = new URLSearchParams(); params.append('term', options.query); if (options.category) { params.append('category', options.category); } return `${baseUrl}?${params.toString()}`; } isValidRapidAPIUrl(url) { const pattern = /^https:\/\/rapidapi\.com\/.+\/api\/.+/; return pattern.test(url); } }