Skip to main content
Glama

Captain Data MCP API

tools.ts8.22 kB
import { FastifyRequest, FastifyReply } from 'fastify'; import { ALIAS_TO_SLUG, ToolAlias } from "../../lib/alias"; import { toCaptainData } from "../../lib/translate"; import { createErrorResponse, ERROR_CODES } from "../../lib/error"; import { config } from "../../lib/config"; import { extractApiKey, createAuthErrorResponse } from "../../lib/auth"; import { logError, logInfo } from "../../middleware"; interface ToolParams { alias: string; } // Configuration for timeouts and retries const API_TIMEOUT = config.apiTimeout; const MAX_RETRIES = config.maxRetries; const RETRY_DELAY = config.retryDelay; // Helper function to delay execution const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); // Helper function to make API call with timeout and retry logic async function makeCaptainDataRequest(url: string, options: RequestInit, retries = 0): Promise<Response> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); // Handle timeout if (error instanceof Error && error.name === 'AbortError') { throw new Error('Request timeout'); } // Handle network errors with retry logic if (retries < MAX_RETRIES && (error instanceof TypeError || (error instanceof Error && error.message.includes('fetch')))) { await delay(RETRY_DELAY * (retries + 1)); return makeCaptainDataRequest(url, options, retries + 1); } throw error; } } export default async function handler(req: FastifyRequest<{ Params: ToolParams }>, reply: FastifyReply) { const startTime = Date.now(); const requestId = req.id || `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Declare variables outside try block for access in catch let alias: ToolAlias | undefined; let body: any = {}; let cdBody: any = {}; try { alias = req.params.alias as ToolAlias; const slug = ALIAS_TO_SLUG[alias]; if (!slug) { return reply.status(404).send( createErrorResponse( ERROR_CODES.UNKNOWN_TOOL, `Tool '${alias}' not supported`, requestId ) ); } body = req.body as any; // Extract API key from either direct header or session token let key: string; try { // Debug logging for authentication const authHeaders = Object.keys(req.headers).filter(key => key.toLowerCase().includes('auth') || key.toLowerCase().includes('api') || key.toLowerCase().includes('key') ); console.log(`[TOOLS DEBUG] Extracting API key for ${alias}:`, { hasXApiKey: !!req.headers['x-api-key'], hasAuthHeader: !!req.headers['authorization'], authHeaderLength: req.headers['authorization'] ? (req.headers['authorization'] as string).length : 0, authHeaderPreview: req.headers['authorization'] ? ((req.headers['authorization'] as string).length > 50 ? (req.headers['authorization'] as string).substring(0, 50) + '...' : req.headers['authorization']) : null, allAuthHeaders: authHeaders, requestId }); key = await extractApiKey(req.headers); console.log(`[TOOLS DEBUG] API key extraction successful for ${alias}:`, { keyLength: key.length, keyPreview: key.substring(0, 8) + '...', requestId }); } catch (authError) { console.error(`[TOOLS DEBUG] API key extraction failed for ${alias}:`, { error: authError instanceof Error ? authError.message : 'Unknown error', requestId, headers: Object.keys(req.headers).filter(key => key.toLowerCase().includes('auth') || key.toLowerCase().includes('api') || key.toLowerCase().includes('key') ) }); return reply.status(401).send( createAuthErrorResponse(authError as Error, requestId) ); } // Server-side validation for searchCompanyEmployees if (alias === 'search_company_employees') { if (!body.sales_navigator_company_url && !body.linkedin_company_id) { return reply.status(400).send( createErrorResponse( ERROR_CODES.MISSING_INPUT, "Must provide either a company URL or ID", requestId ) ); } } cdBody = toCaptainData(alias, body); const apiUrl = `${config.cdApiBase}/v4/actions/${slug}/run/live`; // Log the request (without sensitive data) logInfo('Executing Captain Data tool', req, { endpoint: 'tools', tool: alias, slug, hasApiKey: !!key, bodyKeys: Object.keys(body), cdBody: JSON.stringify(cdBody), apiUrl }); const cdRes = await makeCaptainDataRequest(apiUrl, { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": key }, body: JSON.stringify(cdBody) }); // Parse response safely let responseData; try { responseData = await cdRes.json(); logInfo('Parsed response data', req, { endpoint: 'tools', tool: alias, responseDataKeys: Object.keys(responseData), responseData: JSON.stringify(responseData) }); } catch (parseError) { logError('Failed to parse Captain Data response', parseError, req, { endpoint: 'tools', tool: alias }); return reply.status(500).send( createErrorResponse( ERROR_CODES.INVALID_RESPONSE, "Invalid response from Captain Data API", requestId ) ); } // Check if it's an error response from Captain Data if (!cdRes.ok) { // Return error response as-is without metadata reply.header('Content-Type', 'application/json'); reply.raw.writeHead(cdRes.status, { 'Content-Type': 'application/json' }); reply.raw.end(JSON.stringify(responseData)); return; } // Add request metadata to successful responses only const responseWithMetadata = { ...responseData, _metadata: { requestId, executionTime: Date.now() - startTime, tool: alias } }; // Log response details for debugging logInfo('Tool execution completed', req, { endpoint: 'tools', tool: alias, status: cdRes.status, responseDataType: typeof responseData, responseDataIsObject: typeof responseData === 'object', finalResponseLength: JSON.stringify(responseWithMetadata).length, finalResponseKeys: Object.keys(responseWithMetadata) }); const finalResponse = { ...responseData, _metadata: { requestId, executionTime: Date.now() - startTime, tool: alias } }; reply.header('Content-Type', 'application/json'); reply.raw.writeHead(cdRes.status, { 'Content-Type': 'application/json' }); reply.raw.end(JSON.stringify(finalResponse)); return; } catch (error) { logError('Tool execution failed', error, req, { endpoint: 'tools', tool: alias || 'unknown', requestBody: body || {}, cdBody: cdBody || {} }); // Handle specific error types if (error instanceof Error) { if (error.message === 'Request timeout') { return reply.status(408).send( createErrorResponse( ERROR_CODES.TIMEOUT, "Request to Captain Data API timed out", requestId ) ); } if (error.message.includes('fetch')) { return reply.status(503).send( createErrorResponse( ERROR_CODES.SERVICE_UNAVAILABLE, "Captain Data API is temporarily unavailable", requestId ) ); } } return reply.status(500).send( createErrorResponse( ERROR_CODES.INTERNAL_ERROR, "An unexpected error occurred", requestId ) ); } }

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/captaindatatech/captaindata-mcp'

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