Skip to main content
Glama

Tinder API MCP Server

request-handler.ts13 kB
/** * Request Handler Service * Processes incoming requests and forwards them to the Tinder API */ import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import { z } from 'zod'; import config from '../config'; import logger from '../utils/logger'; import { ApiError } from '../utils/error-handler'; import { ErrorCodes, ClientRequest } from '../types'; import authService from './authentication'; import cacheManager from './cache-manager'; import rateLimiter from './rate-limiter'; import validationService from '../utils/validation'; import { schemaRegistry } from '../schemas/registry'; import baseSchema from '../schemas/common/base.schema'; import { sanitizeRequestBody } from '../utils/sanitizer'; /** * UUID regex pattern for validation */ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** * Client request schema */ const clientRequestSchema = z.object({ method: z.enum(['GET', 'POST', 'PUT', 'DELETE']), endpoint: z.string().min(1), headers: z.record(z.string(), z.unknown()).optional(), body: z.any().optional(), params: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(), userId: z.string().regex(UUID_REGEX, 'Invalid UUID format').optional() }); /** * Endpoint-specific schemas */ const endpointSchemas: Record<string, z.ZodType> = { '/v2/auth/sms/send': schemaRegistry.getSchema('auth.sms.request') || z.any(), '/v2/auth/sms/validate': schemaRegistry.getSchema('auth.otpVerification.request') || z.any(), '/v2/auth/login/sms': schemaRegistry.getSchema('auth.refreshToken.request') || z.any(), '/v2/auth/login/facebook': schemaRegistry.getSchema('auth.facebook.request') || z.any(), '/v2/auth/verify-captcha': z.object({ captcha_input: z.string().min(1), vendor: z.enum(['arkose', 'recaptcha']) }), '/like': schemaRegistry.getSchema('interaction.like.request') || z.any(), '/pass': schemaRegistry.getSchema('interaction.pass.request') || z.any(), '/superlike': schemaRegistry.getSchema('interaction.superLike.request') || z.any(), '/boost': z.object({}), // Boost endpoint doesn't require body '/user': z.object({}), // GET user profile doesn't require body '/v2/recs/core': z.object({}) // GET recommendations doesn't require body }; /** * Request Handler class * Processes client requests and forwards them to Tinder API */ class RequestHandler { private baseUrl: string; private httpClient: AxiosInstance; constructor() { this.baseUrl = config.TINDER_API.BASE_URL; this.httpClient = axios.create({ baseURL: this.baseUrl, timeout: config.TINDER_API.TIMEOUT }); // Configure retry logic this.httpClient.interceptors.response.use(null, async (error: AxiosError) => { const config = error.config as AxiosRequestConfig & { retry?: number; maxRetries?: number }; // Only retry on network errors or 5xx errors if (!config || !config.retry || config.retry >= (config.maxRetries || 0) || (error.response && error.response.status < 500)) { return Promise.reject(error); } // Set retry count config.retry = config.retry ? config.retry + 1 : 1; // Exponential backoff const delay = Math.pow(2, config.retry) * 1000; logger.info(`Retrying request to ${config.url} (attempt ${config.retry})`); // Delay and retry return new Promise(resolve => setTimeout(() => resolve(this.httpClient(config)), delay)); }); } /** * Process client request and forward to Tinder API * @param clientRequest - Client request object * @returns API response */ public async processRequest(clientRequest: ClientRequest): Promise<any> { try { // Validate request structure this.validateRequest(clientRequest); // Check request body size if (clientRequest.body) { const bodySize = JSON.stringify(clientRequest.body).length; const maxSize = 100 * 1024; // 100KB if (bodySize > maxSize) { throw new ApiError( ErrorCodes.VALIDATION_ERROR, `Request body size exceeds maximum allowed size of ${maxSize} bytes`, { size: bodySize, maxSize }, 413 ); } } // Sanitize request body and params to prevent injection attacks if (clientRequest.body) { clientRequest.body = sanitizeRequestBody(clientRequest.body); } if (clientRequest.params) { clientRequest.params = sanitizeRequestBody(clientRequest.params); } // Validate request body against endpoint-specific schema if available this.validateRequestBody(clientRequest); // Check rate limits await rateLimiter.checkRateLimit(clientRequest.endpoint, clientRequest.userId); // Check if authentication is required if (this.requiresAuthentication(clientRequest.endpoint) && clientRequest.userId) { // Get token or refresh if needed const token = await authService.getValidToken(clientRequest.userId); clientRequest.headers = clientRequest.headers || {}; clientRequest.headers['X-Auth-Token'] = token; } // Add standard headers this.addStandardHeaders(clientRequest); // Check cache for GET requests if (clientRequest.method === 'GET' && this.isCacheable(clientRequest.endpoint)) { const cachedResponse = await cacheManager.get(this.getCacheKey(clientRequest)); if (cachedResponse) { logger.debug(`Cache hit for ${clientRequest.endpoint}`); return cachedResponse; } } // Prepare request config const requestConfig: AxiosRequestConfig & { maxRetries?: number } = { method: clientRequest.method, url: clientRequest.endpoint, headers: clientRequest.headers, data: clientRequest.body, params: clientRequest.params, maxRetries: config.TINDER_API.MAX_RETRIES }; // Send request to Tinder API logger.info(`Sending ${clientRequest.method} request to ${clientRequest.endpoint}`); const response = await this.httpClient(requestConfig); // Cache response if applicable if (clientRequest.method === 'GET' && this.isCacheable(clientRequest.endpoint)) { await cacheManager.set(this.getCacheKey(clientRequest), response.data); } // Update rate limit info rateLimiter.updateRateLimits(clientRequest.endpoint, response, clientRequest.userId); // Decrement rate limit counter for successful actions if (clientRequest.userId && response.status >= 200 && response.status < 300) { rateLimiter.decrementRateLimit(clientRequest.endpoint, clientRequest.userId); } return response.data; } catch (error) { logger.error(`Request processing error: ${(error as Error).message}`); if ((error as AxiosError).response) { // Handle API error responses const statusCode = (error as AxiosError).response!.status; // Handle authentication errors if (statusCode === 401 && clientRequest.userId) { logger.warn(`Authentication failed for user ${clientRequest.userId}, removing token`); // Remove invalid token authService.removeToken(clientRequest.userId); } throw new ApiError( this.mapStatusCodeToErrorCode(statusCode), ((error as AxiosError).response!.data as any)?.message || 'API request failed', (error as AxiosError).response!.data, statusCode ); } else if ((error as AxiosError).code === 'ECONNABORTED') { throw new ApiError( ErrorCodes.NETWORK_ERROR, 'Request timeout', { timeout: config.TINDER_API.TIMEOUT }, 408 ); } else { throw error; // Re-throw other errors } } } /** * Validate client request structure * @param request - Client request * @throws {ApiError} If validation fails */ private validateRequest(request: ClientRequest): void { // Use Zod to validate the request structure const result = clientRequestSchema.safeParse(request); if (!result.success) { const errorMessage = validationService.formatZodError(result.error); throw new ApiError( ErrorCodes.VALIDATION_ERROR, `Invalid request: ${errorMessage}`, { details: result.error.issues }, 400 ); } // Additional endpoint-specific validations if (request.endpoint.includes('/like/') && request.method === 'GET') { // Validate user ID for like endpoint const userId = request.endpoint.split('/like/')[1]; if (!userId) { throw new ApiError( ErrorCodes.VALIDATION_ERROR, 'User ID is required for like endpoint', null, 400 ); } // Validate UUID format const uuidResult = baseSchema.uuidString.safeParse(userId); if (!uuidResult.success) { throw new ApiError( ErrorCodes.VALIDATION_ERROR, 'Invalid user ID format', { details: uuidResult.error.issues }, 400 ); } } } /** * Validate request body against endpoint-specific schema * @param request - Client request * @throws {ApiError} If validation fails */ private validateRequestBody(request: ClientRequest): void { // Skip validation for GET requests or if no body is provided if (request.method === 'GET' || !request.body) { return; } // Find matching schema for the endpoint const schema = this.findSchemaForEndpoint(request.endpoint); if (schema) { const result = schema.safeParse(request.body); if (!result.success) { const errorMessage = validationService.formatZodError(result.error); throw new ApiError( ErrorCodes.VALIDATION_ERROR, `Invalid request body: ${errorMessage}`, { details: result.error.issues }, 400 ); } // Replace request body with validated data request.body = result.data; } } /** * Find schema for endpoint * @param endpoint - API endpoint * @returns Schema or undefined */ private findSchemaForEndpoint(endpoint: string): z.ZodType | undefined { // Check for exact match if (endpoint in endpointSchemas) { return endpointSchemas[endpoint]; } // Check for pattern match for (const [pattern, schema] of Object.entries(endpointSchemas)) { if (endpoint.includes(pattern)) { return schema; } } return undefined; } /** * Check if endpoint requires authentication * @param endpoint - API endpoint * @returns True if authentication is required */ private requiresAuthentication(endpoint: string): boolean { // List of endpoints that don't require authentication const publicEndpoints = [ '/v2/auth/sms/send', '/v2/auth/sms/validate', '/v2/auth/login/sms', '/v2/auth/login/facebook', '/v2/auth/verify-captcha' ]; return !publicEndpoints.some(e => endpoint.startsWith(e)); } /** * Add standard headers to request * @param request - Client request */ private addStandardHeaders(request: ClientRequest): void { request.headers = { ...request.headers, 'app-version': '1020345', 'platform': 'web', 'content-type': 'application/json', 'x-supported-image-formats': 'webp,jpeg', 'user-agent': 'Tinder/1020345 (web) MCP-Server/1.0.0' }; } /** * Check if endpoint response is cacheable * @param endpoint - API endpoint * @returns True if cacheable */ private isCacheable(endpoint: string): boolean { // List of endpoints that can be cached const cacheableEndpoints = [ '/user/', '/v2/recs/core' ]; return cacheableEndpoints.some(e => endpoint.startsWith(e)); } /** * Generate cache key for request * @param request - Client request * @returns Cache key */ private getCacheKey(request: ClientRequest): string { return `${request.endpoint}:${JSON.stringify(request.params || {})}:${request.userId || 'anonymous'}`; } /** * Map HTTP status code to internal error code * @param statusCode - HTTP status code * @returns Internal error code */ private mapStatusCodeToErrorCode(statusCode: number): ErrorCodes { switch (statusCode) { case 401: return ErrorCodes.AUTHENTICATION_FAILED; case 429: return ErrorCodes.RATE_LIMIT_EXCEEDED; case 400: return ErrorCodes.VALIDATION_ERROR; default: return ErrorCodes.API_ERROR; } } } // Export singleton instance export default new RequestHandler();

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/glassBead-tc/tinder-mcp-server'

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