UNPKG

@equidam/mcp-server

Version:

Equidam MCP Server - Bridge between AI assistants and Equidam's company valuation API

383 lines (333 loc) 12.3 kB
/** * HTTP client for Equidam API with retry logic */ const axios = require("axios"); const https = require("https"); const { convertAxiosError, formatUserError } = require("./utils/errors"); const { withRetry, HTTP_RETRY_CONFIG } = require("./utils/retry"); /** * API endpoints configuration */ const API_ENDPOINTS = { CLASSIFY: "/mcp/tools/classify_company_industry", SELECT_INDUSTRY: "/mcp/tools/select_industry_classification", VALUATE: "/mcp/tools/get_company_valuation", }; /** * Equidam API Client */ class EquidamApiClient { /** * Create a new API client instance * @param {object} config - Configuration object * @param {string} config.apiKey - Equidam API key * @param {string} config.baseUrl - Base URL for API requests * @param {Function} config.debugLog - Debug logging function */ constructor({ apiKey, baseUrl = "https://app.equidam.com/api/v3", debugLog = () => {} }) { this.apiKey = apiKey; this.baseUrl = baseUrl.replace(/\/$/, ""); // Remove trailing slash this.debugLog = debugLog; // Create axios instance with default configuration const axiosConfig = { baseURL: this.baseUrl, timeout: 30000, // 30 seconds timeout headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", Accept: "application/json", "User-Agent": "@equidam/mcp-server/1.0.0", }, }; // For development environments with self-signed certificates const isDevelopment = this.baseUrl.includes("localhost") || this.baseUrl.includes("127.0.0.1") || this.baseUrl.includes(".local") || this.baseUrl.includes(".test") || this.baseUrl.includes("192.168."); if (isDevelopment) { axiosConfig.httpsAgent = new https.Agent({ rejectUnauthorized: false, }); this.debugLog("WARNING: SSL certificate verification disabled for development environment"); this.debugLog("Base URL detected as development:", this.baseUrl); } this.httpClient = axios.create(axiosConfig); // Add request interceptor for debugging this.httpClient.interceptors.request.use( (config) => { this.debugLog("Making API request:", { method: config.method?.toUpperCase(), url: config.url, data: config.data ? JSON.stringify(config.data) : undefined, }); return config; }, (error) => { this.debugLog("Request interceptor error:", error.message); return Promise.reject(error); } ); // Add response interceptor for debugging and error handling this.httpClient.interceptors.response.use( (response) => { this.debugLog("API response received:", { status: response.status, statusText: response.statusText, data: response.data ? JSON.stringify(response.data) : undefined, }); return response; }, (error) => { this.debugLog("API response error:", { status: error.response?.status, statusText: error.response?.statusText, message: error.message, data: error.response?.data ? JSON.stringify(error.response.data) : undefined, }); return Promise.reject(error); } ); } /** * Make a raw HTTP request with retry logic * @param {object} requestConfig - Axios request configuration * @returns {Promise} - Promise that resolves to response data */ async makeRequest(requestConfig) { const requestFn = async () => { try { const response = await this.httpClient.request(requestConfig); return response.data; } catch (error) { // Convert axios error to our custom error types throw convertAxiosError(error); } }; try { return await withRetry(requestFn, HTTP_RETRY_CONFIG, this.debugLog); } catch (error) { this.debugLog("Final API request error:", error.message); throw error; } } /** * Classify company industry from description * @param {object} params - Classification parameters * @param {string} params.description - Company description * @param {string[]} [params.context] - Additional context * @returns {Promise<object>} - Classification result */ async classifyIndustry({ description, context }) { this.validateClassifyParams({ description, context }); try { const data = await this.makeRequest({ method: "POST", url: API_ENDPOINTS.CLASSIFY, data: { description: description.trim(), ...(context && context.length > 0 && { context }), }, }); return this.normalizeClassificationResponse(data); } catch (error) { const userMessage = formatUserError(error); this.debugLog("Industry classification failed:", userMessage); throw new Error(`Industry classification failed: ${userMessage}`); } } /** * Select industry classification from multiple options * @param {object} params - Selection parameters * @param {string} params.industry_code - Selected industry code * @param {string} [params.user_confirmation] - Optional user confirmation * @returns {Promise<object>} - Selection confirmation result */ async selectIndustryClassification({ industry_code, user_confirmation }) { this.validateSelectIndustryParams({ industry_code, user_confirmation }); try { const data = await this.makeRequest({ method: "POST", url: API_ENDPOINTS.SELECT_INDUSTRY, data: { industry_code: industry_code.trim(), ...(user_confirmation && { user_confirmation: user_confirmation.trim() }), }, }); return this.normalizeSelectionResponse(data); } catch (error) { const userMessage = formatUserError(error); this.debugLog("Industry selection failed:", userMessage); throw new Error(`Industry selection failed: ${userMessage}`); } } /** * Get company valuation * @param {object} params - Valuation parameters * @returns {Promise<object>} - Valuation result */ async getValuation(params) { this.validateValuationParams(params); try { const data = await this.makeRequest({ method: "POST", url: API_ENDPOINTS.VALUATE, data: this.normalizeValuationParams(params), }); return this.normalizeValuationResponse(data); } catch (error) { const userMessage = formatUserError(error); this.debugLog("Valuation request failed:", userMessage); throw new Error(`Valuation request failed: ${userMessage}`); } } /** * Validate classification parameters * @param {object} params - Parameters to validate */ validateClassifyParams({ description, context }) { if (!description || typeof description !== "string") { throw new Error("Description is required and must be a string"); } if (description.trim().length < 3) { throw new Error("Description must be at least 3 characters long"); } if (description.length > 1000) { throw new Error("Description must be less than 1000 characters"); } if (context && !Array.isArray(context)) { throw new Error("Context must be an array of strings"); } if (context && context.some((item) => typeof item !== "string")) { throw new Error("All context items must be strings"); } } /** * Validate industry selection parameters * @param {object} params - Parameters to validate */ validateSelectIndustryParams({ industry_code, user_confirmation }) { if (!industry_code || typeof industry_code !== "string") { throw new Error("industry_code is required and must be a string"); } if (!/^[0-9]{10}$/.test(industry_code.trim())) { throw new Error("industry_code must be exactly 10 digits"); } if (user_confirmation && typeof user_confirmation !== "string") { throw new Error("user_confirmation must be a string"); } if (user_confirmation && user_confirmation.length > 500) { throw new Error("user_confirmation must be less than 500 characters"); } } /** * Validate valuation parameters * @param {object} params - Parameters to validate */ validateValuationParams(params) { const required = ["industry_code", "revenue_y1", "revenue_y4", "country"]; for (const field of required) { if (params[field] === undefined || params[field] === null) { throw new Error(`${field} is required`); } } // Validate industry code format (10 digits) if (!/^[0-9]{10}$/.test(params.industry_code)) { throw new Error("industry_code must be exactly 10 digits"); } // Validate revenue values if (typeof params.revenue_y1 !== "number" || params.revenue_y1 < 0) { throw new Error("revenue_y1 must be a non-negative number"); } if (typeof params.revenue_y4 !== "number" || params.revenue_y4 < 0) { throw new Error("revenue_y4 must be a non-negative number"); } // Validate country code format (2 uppercase letters) if (!/^[A-Z]{2}$/.test(params.country)) { throw new Error("country must be a 2-letter uppercase country code (e.g., US, GB)"); } // Validate optional parameters if (params.currency && !/^[A-Z]{3}$/.test(params.currency)) { throw new Error("currency must be a 3-letter uppercase currency code (e.g., USD, EUR)"); } if (params.employees !== undefined && (!Number.isInteger(params.employees) || params.employees < 0)) { throw new Error("employees must be a non-negative integer"); } if (params.founders !== undefined && (!Number.isInteger(params.founders) || params.founders < 0)) { throw new Error("founders must be a non-negative integer"); } if (params.started_year !== undefined && (!Number.isInteger(params.started_year) || params.started_year < 1800 || params.started_year > new Date().getFullYear() + 1)) { throw new Error(`started_year must be a valid year between 1800 and ${new Date().getFullYear() + 1}`); } } /** * Normalize valuation parameters for API * @param {object} params - Raw parameters * @returns {object} - Normalized parameters */ normalizeValuationParams(params) { const normalized = { industry_code: params.industry_code, revenue_y1: params.revenue_y1, revenue_y4: params.revenue_y4, country: params.country.toUpperCase(), }; // Add optional parameters if provided if (params.currency) { normalized.currency = params.currency.toUpperCase(); } if (params.employees !== undefined) { normalized.employees = params.employees; } if (params.founders !== undefined) { normalized.founders = params.founders; } if (params.started_year !== undefined) { normalized.started_year = params.started_year; } return normalized; } /** * Normalize classification response * @param {object} data - Raw API response * @returns {object} - Normalized response */ normalizeClassificationResponse(data) { return { industry_code: data.industry_code || "", industry_name: data.industry_name || "", confidence: data.confidence || 0, alternatives: data.alternatives || [], }; } /** * Normalize selection response * @param {object} data - Raw API response * @returns {object} - Normalized response */ normalizeSelectionResponse(data) { return { status: data.status || "classification_confirmed", industry_code: data.industry_code || "", industry_name: data.industry_name || "", description: data.description || "", message: data.message || "", usage_note: data.usage_note || "", llm_guidance: data.llm_guidance || {}, ...data, // Include any additional fields from the API }; } /** * Normalize valuation response * @param {object} data - Raw API response * @returns {object} - Normalized response */ normalizeValuationResponse(data) { return { valuation: data.valuation || {}, methodology: data.methodology || {}, market_data: data.market_data || {}, ...data, // Include any additional fields from the API }; } } module.exports = { EquidamApiClient, API_ENDPOINTS, };