rapidapi-mcp-server
Version:
MCP server for discovering and assessing APIs from RapidAPI marketplace
309 lines (308 loc) • 16.6 kB
JavaScript
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);
}
}