UNPKG

mcp-tenant-credit-scorer

Version:

MCP server for tenant credit scoring based on S&P corporate methodology

263 lines (217 loc) 7.45 kB
import { readFile } from 'fs/promises'; import fetch from 'node-fetch'; import * as cheerio from 'cheerio'; export class DataParser { constructor() { this.financialPatterns = { revenue: /(?:revenue|sales|turnover)[\s:]+\$?([\d,]+\.?\d*)\s*(?:million|M|thousand|K)?/i, ebitda: /(?:ebitda|earnings before)[\s:]+\$?([\d,]+\.?\d*)\s*(?:million|M|thousand|K)?/i, netIncome: /(?:net income|net profit|net earnings)[\s:]+\$?([\d,]+\.?\d*)\s*(?:million|M|thousand|K)?/i, totalDebt: /(?:total debt|long.?term debt)[\s:]+\$?([\d,]+\.?\d*)\s*(?:million|M|thousand|K)?/i, currentAssets: /(?:current assets)[\s:]+\$?([\d,]+\.?\d*)\s*(?:million|M|thousand|K)?/i, currentLiabilities: /(?:current liabilities)[\s:]+\$?([\d,]+\.?\d*)\s*(?:million|M|thousand|K)?/i, }; } async parsePDF(pdfPath) { try { // Lazy load pdf-parse to avoid initialization issues const { default: pdfParse } = await import('pdf-parse'); const dataBuffer = await readFile(pdfPath); const data = await pdfParse(dataBuffer); const text = data.text; // Extract financial data from PDF text const financials = this.extractFinancialData(text); // Try to identify multiple years of data const multiYearData = this.extractMultiYearData(text); return { rawText: text, extractedFinancials: financials, multiYearData: multiYearData, pageCount: data.numpages }; } catch (error) { throw new Error(`Failed to parse PDF: ${error.message}`); } } async parseWebsite(url) { try { const response = await fetch(url); const html = await response.text(); const $ = cheerio.load(html); // Extract company information const companyInfo = { title: $('title').text(), description: $('meta[name="description"]').attr('content') || '', about: this.extractAboutText($), industry: this.extractIndustryHints($), employees: this.extractEmployeeCount($), locations: this.extractLocations($), products: this.extractProducts($) }; return companyInfo; } catch (error) { console.error(`Failed to parse website: ${error.message}`); return {}; } } extractFinancialData(text) { const financials = {}; for (const [key, pattern] of Object.entries(this.financialPatterns)) { const match = text.match(pattern); if (match) { let value = parseFloat(match[1].replace(/,/g, '')); // Handle multipliers if (match[0].toLowerCase().includes('million') || match[0].includes('M')) { value *= 1000000; } else if (match[0].toLowerCase().includes('thousand') || match[0].includes('K')) { value *= 1000; } financials[key] = value; } } // Calculate derived metrics if (financials.currentAssets && financials.currentLiabilities) { financials.currentRatio = financials.currentAssets / financials.currentLiabilities; } if (financials.ebitda && financials.revenue) { financials.ebitdaMargin = (financials.ebitda / financials.revenue) * 100; } return financials; } extractMultiYearData(text) { // Look for year patterns (e.g., 2021, 2022, 2023) const yearPattern = /20\d{2}/g; const years = [...new Set(text.match(yearPattern))].sort().slice(-3); if (years.length < 2) { return null; } const multiYearData = {}; years.forEach(year => { // Try to extract data for each year const yearSection = text.split(year)[1]?.substring(0, 1000) || ''; multiYearData[year] = this.extractFinancialData(yearSection); }); return multiYearData; } extractAboutText($) { // Common selectors for about sections const selectors = [ '.about-us', '#about', '[class*="about"]', 'section:contains("About")', 'div:contains("Who We Are")' ]; for (const selector of selectors) { const text = $(selector).text().trim(); if (text && text.length > 50) { return text.substring(0, 500); } } return ''; } extractIndustryHints($) { const hints = []; // Look for industry keywords in various places const industryKeywords = [ 'technology', 'software', 'healthcare', 'manufacturing', 'retail', 'financial services', 'real estate', 'energy', 'construction', 'transportation', 'hospitality', 'education', 'consulting' ]; const textContent = $('body').text().toLowerCase(); industryKeywords.forEach(keyword => { if (textContent.includes(keyword)) { hints.push(keyword); } }); return hints; } extractEmployeeCount($) { const employeePatterns = [ /(\d+)\s*employees/i, /team of\s*(\d+)/i, /staff of\s*(\d+)/i ]; const bodyText = $('body').text(); for (const pattern of employeePatterns) { const match = bodyText.match(pattern); if (match) { return parseInt(match[1]); } } return null; } extractLocations($) { const locations = []; // Look for address patterns const addressSelectors = [ 'address', '.address', '[class*="location"]', '[class*="office"]' ]; addressSelectors.forEach(selector => { $(selector).each((i, elem) => { const text = $(elem).text().trim(); if (text && text.length > 10) { locations.push(text); } }); }); return [...new Set(locations)]; } extractProducts($) { const products = []; // Look for product/service sections const productSelectors = [ '.products', '.services', '[class*="product"]', '[class*="service"]', 'section:contains("What We Do")', 'section:contains("Our Services")' ]; productSelectors.forEach(selector => { $(selector).find('h2, h3, h4, li').each((i, elem) => { const text = $(elem).text().trim(); if (text && text.length > 5 && text.length < 100) { products.push(text); } }); }); return [...new Set(products)].slice(0, 10); } normalizeFinancialData(data) { // Ensure all required fields exist with defaults const normalized = { revenue: 0, ebitda: 0, ebitdaMargin: 0, netIncome: 0, totalDebt: 0, cash: 0, currentAssets: 0, currentLiabilities: 0, currentRatio: 0, interestExpense: 0, ...data }; // Calculate missing metrics if possible if (normalized.revenue && normalized.ebitda && !normalized.ebitdaMargin) { normalized.ebitdaMargin = (normalized.ebitda / normalized.revenue) * 100; } if (normalized.currentAssets && normalized.currentLiabilities && !normalized.currentRatio) { normalized.currentRatio = normalized.currentAssets / normalized.currentLiabilities; } if (normalized.totalDebt && normalized.cash) { normalized.netDebt = normalized.totalDebt - normalized.cash; } if (normalized.ebitda && normalized.interestExpense) { normalized.ebitdaToInterest = normalized.ebitda / normalized.interestExpense; } if (normalized.ebitda && normalized.netDebt) { normalized.netDebtToEbitda = normalized.netDebt / normalized.ebitda; } return normalized; } }