UNPKG

mcp-tenant-credit-scorer

Version:

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

307 lines (274 loc) 9.41 kB
import { readFile } from 'fs/promises'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export class IndustryMatcher { constructor() { this.industryData = null; this.initialized = false; } async initialize() { if (this.initialized) return; try { const dataPath = join(__dirname, '..', 'data', 'industry-data.json'); const data = await readFile(dataPath, 'utf-8'); this.industryData = JSON.parse(data); this.initialized = true; } catch (error) { console.error('Failed to load industry data:', error); // Use a simplified fallback dataset this.industryData = this.getFallbackData(); this.initialized = true; } } async classify(companyInfo) { await this.initialize(); const { description, website, products, industryHints } = companyInfo; // Combine all text for analysis const fullText = [ description || '', ...(products || []), ...(industryHints || []) ].join(' ').toLowerCase(); // Score each industry based on keyword matches const industryScores = {}; for (const industry of this.industryData.industries) { let score = 0; // Check primary keywords industry.keywords?.forEach(keyword => { if (fullText.includes(keyword.toLowerCase())) { score += 3; } }); // Check secondary keywords industry.secondaryKeywords?.forEach(keyword => { if (fullText.includes(keyword.toLowerCase())) { score += 1; } }); // Check industry description match if (fullText.includes(industry.primaryIndustry.toLowerCase())) { score += 5; } if (score > 0) { industryScores[industry.primaryIndustry] = score; } } // Find best match let bestMatch = null; let highestScore = 0; for (const [industry, score] of Object.entries(industryScores)) { if (score > highestScore) { highestScore = score; bestMatch = industry; } } // Get full classification details const classification = this.getClassificationDetails(bestMatch); return { ...classification, confidence: this.calculateConfidence(highestScore), alternativeMatches: this.getAlternativeMatches(industryScores, bestMatch) }; } getClassificationDetails(primaryIndustry) { const industry = this.industryData.industries.find( ind => ind.primaryIndustry === primaryIndustry ); if (!industry) { return this.getDefaultClassification(); } // Get industry risk score const industryRisk = this.calculateIndustryRisk(industry); return { sector: industry.sector, industryGroup: industry.industryGroup, primaryIndustry: industry.primaryIndustry, description: industry.description, cpSector: industry.cpSector, cpIndustryGroup: industry.cpIndustryGroup, defaultCPGP: this.mapCPSectorToCPGP(industry.cpSector), industryRisk: industryRisk, typicalCharacteristics: industry.characteristics || [] }; } calculateIndustryRisk(industry) { // Map industries to risk scores based on cyclicality const riskMappings = { 'Pharmaceuticals': 5, 'Branded nondurables': 5, 'Health care equipment': 5, 'Telecommunications': 5, 'Regulated utilities': 4, 'Environmental services': 4, 'Healthcare services': 4, 'Retail': 4, 'Media and entertainment': 4, 'Technology software': 4, 'Capital goods': 3, 'Technology hardware': 3, 'Chemicals': 3, 'Building materials': 3, 'Business services': 3, 'Oil and gas refining': 2, 'Homebuilders': 2, 'Metals and mining': 1, 'Auto OEM': 1, 'Transportation cyclical': 1 }; // Check for exact matches for (const [key, risk] of Object.entries(riskMappings)) { if (industry.cpIndustryGroup?.toLowerCase().includes(key.toLowerCase()) || industry.primaryIndustry?.toLowerCase().includes(key.toLowerCase())) { return { score: risk, riskLevel: this.getRiskLevelName(risk), cyclicality: this.getCyclicalityLevel(risk), ebitdaDeclineRange: this.getEBITDADeclineRange(risk) }; } } // Default to intermediate risk return { score: 3, riskLevel: 'Intermediate Risk', cyclicality: 'Moderate', ebitdaDeclineRange: '10-19.9%' }; } mapCPSectorToCPGP(cpSector) { const mapping = { 'Services & Product Focus': 'Services & Product Focus', 'Product Focus/Scale Driven': 'Product Focus/Scale Driven', 'Capital or Asset Focus': 'Capital or Asset Focus', 'Commodity Focus/Cost Driven': 'Commodity Focus/Cost Driven', 'Commodity Focus/Scale Driven': 'Commodity Focus/Scale Driven', 'Service & Product Focus': 'Services & Product Focus', // Handle variant 'National Industries and Utilities': 'Capital or Asset Focus' // Default for utilities }; return mapping[cpSector] || 'Services & Product Focus'; } getRiskLevelName(score) { const levels = { 5: 'Very Low Risk', 4: 'Low Risk', 3: 'Intermediate Risk', 2: 'Moderately High Risk', 1: 'High Risk' }; return levels[score] || 'Unknown'; } getCyclicalityLevel(score) { const levels = { 5: 'Very Low', 4: 'Low', 3: 'Moderate', 2: 'High', 1: 'Very High' }; return levels[score] || 'Unknown'; } getEBITDADeclineRange(score) { const ranges = { 5: '<5%', 4: '5-9.9%', 3: '10-19.9%', 2: '20-29.9%', 1: '≥30%' }; return ranges[score] || 'Unknown'; } calculateConfidence(score) { if (score >= 10) return 'High'; if (score >= 5) return 'Medium'; if (score > 0) return 'Low'; return 'No Match'; } getAlternativeMatches(scores, primaryMatch) { return Object.entries(scores) .filter(([industry]) => industry !== primaryMatch) .sort(([, a], [, b]) => b - a) .slice(0, 3) .map(([industry, score]) => ({ industry, score, classification: this.getClassificationDetails(industry) })); } getDefaultClassification() { return { sector: 'Unknown', industryGroup: 'Unknown', primaryIndustry: 'Business Services (Default)', description: 'Unable to determine specific industry', cpSector: 'Services & Product Focus', cpIndustryGroup: 'Business and consumer services', defaultCPGP: 'Services & Product Focus', industryRisk: { score: 3, riskLevel: 'Intermediate Risk', cyclicality: 'Moderate', ebitdaDeclineRange: '10-19.9%' }, typicalCharacteristics: [] }; } getFallbackData() { // Simplified industry data for fallback return { industries: [ { sector: 'Technology', industryGroup: 'Software and Services', primaryIndustry: 'Software', description: 'Companies developing and producing software', cpSector: 'Services & Product Focus', cpIndustryGroup: 'Technology software and services', keywords: ['software', 'saas', 'application', 'platform', 'cloud'], secondaryKeywords: ['technology', 'digital', 'solution'] }, { sector: 'Consumer Discretionary', industryGroup: 'Consumer Services', primaryIndustry: 'Restaurants', description: 'Owners and operators of restaurants and food service', cpSector: 'Services & Product Focus', cpIndustryGroup: 'Retail and restaurants', keywords: ['restaurant', 'food service', 'dining', 'catering'], secondaryKeywords: ['hospitality', 'food', 'beverage'] }, { sector: 'Industrials', industryGroup: 'Capital Goods', primaryIndustry: 'Industrial Machinery', description: 'Manufacturers of industrial equipment', cpSector: 'Capital or Asset Focus', cpIndustryGroup: 'Capital goods', keywords: ['manufacturing', 'industrial', 'machinery', 'equipment'], secondaryKeywords: ['production', 'factory', 'assembly'] }, { sector: 'Health Care', industryGroup: 'Health Care Equipment and Services', primaryIndustry: 'Health Care Services', description: 'Providers of healthcare services', cpSector: 'Services & Product Focus', cpIndustryGroup: 'Health care services', keywords: ['healthcare', 'medical', 'clinic', 'hospital', 'health'], secondaryKeywords: ['patient', 'care', 'treatment'] }, { sector: 'Real Estate', industryGroup: 'Real Estate Management and Development', primaryIndustry: 'Real Estate Services', description: 'Real estate agents and brokers', cpSector: 'Services & Product Focus', cpIndustryGroup: 'Business and consumer services', keywords: ['real estate', 'property', 'realty', 'brokerage'], secondaryKeywords: ['commercial', 'residential', 'leasing'] } ] }; } }