Skip to main content
Glama

mcp-google-sheets

serp-api-client.ts9.98 kB
import { httpClient, HttpMethod, HttpRequest } from '@activepieces/pieces-common'; import { GoogleSearchConfig, RequestOptions, SerpApiConfig, SerpApiEngine, SerpApiResponse, } from '../types'; import { SerpApiValidator } from '../utils/validators'; // Configuration for the SerpApi client export interface SerpApiClientConfig { /** Base URL for SerpApi */ baseUrl?: string; /** Default timeout for requests */ defaultTimeout?: number; /** Default number of retries */ defaultRetries?: number; /** Default delay between retries (ms) */ defaultRetryDelay?: number; /** Enable request/response logging */ enableLogging?: boolean; } // Creates an appropriate error based on the HTTP status code and response function createErrorFromResponse(statusCode: number, response: any, message?: string): Error { const defaultMessage = message || 'API request failed'; const responseMessage = response?.error || response?.message; const finalMessage = responseMessage || defaultMessage; const error = new Error(finalMessage); (error as any).statusCode = statusCode; (error as any).response = response; switch (statusCode) { case 401: (error as any).code = 'SERP_API_AUTH_ERROR'; break; case 400: (error as any).code = 'SERP_API_VALIDATION_ERROR'; break; case 402: (error as any).code = 'SERP_API_QUOTA_EXCEEDED_ERROR'; break; case 408: (error as any).code = 'SERP_API_TIMEOUT_ERROR'; break; case 429: (error as any).code = 'SERP_API_RATE_LIMIT_ERROR'; (error as any).retryAfter = response?.retry_after || response?.retryAfter; break; default: (error as any).code = 'SERP_API_RESPONSE_ERROR'; break; } return error; } // Creates a network error from a connection/network issue function createNetworkError(originalError: Error): Error { const error = new Error(`Network error: ${originalError.message}`); (error as any).code = 'SERP_API_NETWORK_ERROR'; (error as any).originalError = originalError.message; return error; } // Creates a timeout error function createTimeoutError(timeout: number): Error { const error = new Error(`Request timed out after ${timeout}ms`); (error as any).code = 'SERP_API_TIMEOUT_ERROR'; (error as any).statusCode = 408; (error as any).timeout = timeout; return error; } // SerpApi client export class SerpApiClient { private readonly baseUrl: string; private readonly defaultTimeout: number; private readonly defaultRetries: number; private readonly defaultRetryDelay: number; private readonly enableLogging: boolean; // Creates a new SerpApi client instance constructor(config: SerpApiClientConfig = {}) { this.baseUrl = config.baseUrl || 'https://serpapi.com/search'; this.defaultTimeout = config.defaultTimeout || 30000; this.defaultRetries = config.defaultRetries || 3; this.defaultRetryDelay = config.defaultRetryDelay || 1000; this.enableLogging = config.enableLogging || false; } // Executes a search request with comprehensive error handling and retry logic async executeSearch<T = any>( config: SerpApiConfig, options: RequestOptions = {} ): Promise<SerpApiResponse<T>> { // Validate and sanitize input const validatedConfig = SerpApiValidator.validateAndSanitize(config); // Apply request options with defaults const requestOptions: Required<RequestOptions> = { timeout: options.timeout || this.defaultTimeout, retries: options.retries || this.defaultRetries, retryDelay: options.retryDelay || this.defaultRetryDelay, }; return this.executeWithRetry<T>(validatedConfig, requestOptions); } // Validates API key by making a test request async validateApiKey(apiKey: string): Promise<boolean> { try { const testConfig: GoogleSearchConfig = { api_key: apiKey, engine: SerpApiEngine.GOOGLE, q: 'test', num: 1, }; await this.makeHttpRequest<any>(testConfig, { timeout: 10000, retries: 1, retryDelay: 0 }); return true; } catch (error: any) { if (error.code === 'SERP_API_AUTH_ERROR') { return false; } // Other errors might indicate network issues, not invalid API key throw error; } } // Executes request with retry logic private async executeWithRetry<T>( config: SerpApiConfig, options: Required<RequestOptions> ): Promise<SerpApiResponse<T>> { let lastError: Error | null = null; for (let attempt = 1; attempt <= options.retries + 1; attempt++) { try { const response = await this.makeHttpRequest<T>(config, options); if (this.enableLogging) { console.log(`SerpApi request successful on attempt ${attempt}:`, { engine: config.engine, query: config.q, responseTime: response.search_metadata?.total_time_taken, }); } return response; } catch (error) { lastError = error as Error; // Don't retry on certain error types if (this.shouldNotRetry(error as Error)) { throw error; } // Don't retry on last attempt if (attempt <= options.retries) { const delay = this.calculateRetryDelay(attempt, options.retryDelay); if (this.enableLogging) { console.warn(`SerpApi request failed on attempt ${attempt}, retrying in ${delay}ms:`, { error: (error as Error).message, attempt, maxRetries: options.retries, }); } await this.sleep(delay); } } } const error = new Error('Request failed after all retry attempts'); (error as any).code = 'SERP_API_RESPONSE_ERROR'; (error as any).statusCode = 500; (error as any).attempts = options.retries + 1; throw lastError || error; } // Makes the actual HTTP request to SerpApi private async makeHttpRequest<T>( config: SerpApiConfig, options: Required<RequestOptions> ): Promise<SerpApiResponse<T>> { try { // Build query parameters, filtering out undefined values const queryParams = this.buildQueryParams(config); const httpRequest: HttpRequest = { method: HttpMethod.GET, url: this.baseUrl, queryParams, timeout: options.timeout, }; if (this.enableLogging) { console.log('Making SerpApi request:', { url: this.baseUrl, params: this.sanitizeParamsForLogging(queryParams), }); } const response = await httpClient.sendRequest(httpRequest); // Handle API-level errors in successful HTTP responses if (response.body?.error) { throw createErrorFromResponse( response.status || 400, response.body, response.body.error ); } return this.transformResponse<T>(response.body); } catch (error) { throw this.handleHttpError(error as Error); } } // Builds query parameters from config, filtering undefined values private buildQueryParams(config: SerpApiConfig): Record<string, string> { const params: Record<string, any> = { ...config }; // Remove undefined values and convert all to strings const cleanParams: Record<string, string> = {}; for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { cleanParams[key] = String(value); } } return cleanParams; } // Transforms raw API response into structured format private transformResponse<T>(rawResponse: any): SerpApiResponse<T> { return { data: rawResponse as T, }; } // Handles and transforms HTTP errors into SerpApi errors private handleHttpError(error: Error): Error { // Handle timeout errors if (error.message.includes('timeout') || error.message.includes('ETIMEDOUT')) { return createTimeoutError(this.defaultTimeout); } // Handle network errors if (error.message.includes('ENOTFOUND') || error.message.includes('ECONNREFUSED')) { return createNetworkError(error); } // Handle HTTP response errors if ((error as any).status) { return createErrorFromResponse( (error as any).status, (error as any).body, error.message ); } // Default to network error for unknown errors return createNetworkError(error); } // Determines if an error should not be retried private shouldNotRetry(error: Error): boolean { const errorCode = (error as any).code; // Don't retry authentication errors if (errorCode === 'SERP_API_AUTH_ERROR') { return true; } // Don't retry validation errors if (errorCode === 'SERP_API_VALIDATION_ERROR') { return true; } // Don't retry quota exceeded errors if (errorCode === 'SERP_API_QUOTA_EXCEEDED_ERROR') { return true; } return false; } // Calculates retry delay with exponential backoff private calculateRetryDelay(attempt: number, baseDelay: number): number { // Exponential backoff: baseDelay * 2^(attempt-1) return baseDelay * Math.pow(2, attempt - 1); } // Sleep utility for retry delays private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } // Sanitizes parameters for logging (removes sensitive data) private sanitizeParamsForLogging(params: Record<string, string>): Record<string, string> { const sanitized = { ...params }; if (sanitized['api_key']) { sanitized['api_key'] = '***REDACTED***'; } return sanitized; } // Returns client configuration information getClientInfo(): SerpApiClientConfig { return { baseUrl: this.baseUrl, defaultTimeout: this.defaultTimeout, defaultRetries: this.defaultRetries, defaultRetryDelay: this.defaultRetryDelay, enableLogging: this.enableLogging, }; } }

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/activepieces/activepieces'

If you have feedback or need assistance with the MCP directory API, please join our Discord server