mcp-tenant-credit-scorer
Version:
MCP server for tenant credit scoring based on S&P corporate methodology
307 lines (274 loc) • 9.41 kB
JavaScript
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']
}
]
};
}
}