@equidam/mcp-server
Version:
Equidam MCP Server - Bridge between AI assistants and Equidam's company valuation API
383 lines (333 loc) • 12.3 kB
JavaScript
/**
* 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,
};