mcp-tenant-credit-scorer
Version:
MCP server for tenant credit scoring based on S&P corporate methodology
370 lines (342 loc) • 10.2 kB
JavaScript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import { DataParser } from './data-parser.js';
import { IndustryMatcher } from './industry-matcher.js';
import { ScoreCalculator } from './score-calculator.js';
import { ReportGenerator } from './report-generator.js';
import { Validator } from './validator.js';
// Initialize server
const server = new Server(
{
name: 'mcp-credit-scorer',
version: '1.0.0',
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Initialize modules
const dataParser = new DataParser();
const industryMatcher = new IndustryMatcher();
const scoreCalculator = new ScoreCalculator();
const reportGenerator = new ReportGenerator();
const validator = new Validator();
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'analyze_credit',
description: 'Analyze tenant credit from financial statements and website',
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'Name of the company to analyze',
},
website: {
type: 'string',
description: 'Company website URL',
},
financialData: {
type: 'object',
description: 'Financial statement data (can be PDF path or parsed data)',
properties: {
pdfPath: { type: 'string' },
parsedData: { type: 'object' }
}
},
options: {
type: 'object',
properties: {
includeDetailedReport: { type: 'boolean', default: true },
confidenceLevel: {
type: 'string',
enum: ['audited', 'company-prepared', 'tax-returns', 'limited'],
default: 'company-prepared'
}
}
}
},
required: ['companyName'],
},
},
{
name: 'score_component',
description: 'Score an individual component of the credit assessment',
inputSchema: {
type: 'object',
properties: {
component: {
type: 'string',
enum: ['industry', 'competitive', 'financial', 'liquidity', 'management'],
description: 'Component to score',
},
data: {
type: 'object',
description: 'Data needed for scoring the component',
},
},
required: ['component', 'data'],
},
},
{
name: 'classify_industry',
description: 'Classify a company into S&P industry categories',
inputSchema: {
type: 'object',
properties: {
companyDescription: {
type: 'string',
description: 'Description of company business activities',
},
website: {
type: 'string',
description: 'Company website for additional context',
},
},
required: ['companyDescription'],
},
},
{
name: 'validate_scores',
description: 'Validate score consistency and flag conflicts',
inputSchema: {
type: 'object',
properties: {
scores: {
type: 'object',
properties: {
industryRisk: { type: 'number' },
competitivePosition: { type: 'number' },
financialRisk: { type: 'number' },
liquidity: { type: 'number' },
management: { type: 'number' }
}
},
},
required: ['scores'],
},
}
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'analyze_credit': {
// Full credit analysis workflow
const { companyName, website, financialData, options = {} } = args;
// 1. Parse financial data
let parsedFinancials;
if (financialData?.pdfPath) {
parsedFinancials = await dataParser.parsePDF(financialData.pdfPath);
} else if (financialData?.parsedData) {
parsedFinancials = financialData.parsedData;
} else {
throw new Error('Financial data required');
}
// 2. Extract company info from website
const companyInfo = website ? await dataParser.parseWebsite(website) : {};
// 3. Classify industry
const industryClass = await industryMatcher.classify({
description: companyInfo.description || '',
website: website
});
// 4. Calculate all component scores
const scores = await scoreCalculator.calculateAllScores({
companyName,
financials: parsedFinancials,
industry: industryClass,
companyInfo
});
// 5. Validate scores
const validation = await validator.validateScores(scores);
// 6. Generate report
const report = options.includeDetailedReport
? await reportGenerator.generateFullReport({
companyName,
scores,
validation,
industryClass,
financials: parsedFinancials,
companyInfo,
confidenceLevel: options.confidenceLevel
})
: await reportGenerator.generateSummary({
companyName,
scores,
validation
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
scores,
validation,
report
}, null, 2),
},
],
};
}
case 'score_component': {
const { component, data } = args;
const score = await scoreCalculator.scoreComponent(component, data);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
component,
score
}, null, 2),
},
],
};
}
case 'classify_industry': {
const { companyDescription, website } = args;
const classification = await industryMatcher.classify({
description: companyDescription,
website
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
classification
}, null, 2),
},
],
};
}
case 'validate_scores': {
const { scores } = args;
const validation = await validator.validateScores(scores);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
validation
}, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message
}, null, 2),
},
],
};
}
});
// List available resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'credit-scorer://methodology',
name: 'Credit Scoring Methodology',
description: 'Complete methodology guide for credit scoring',
mimeType: 'text/markdown',
},
{
uri: 'credit-scorer://scoring-tables',
name: 'Scoring Reference Tables',
description: 'All scoring matrices and thresholds',
mimeType: 'application/json',
},
{
uri: 'credit-scorer://industry-data',
name: 'Industry Classifications',
description: 'S&P industry classifications and CPGP assignments',
mimeType: 'application/json',
},
],
};
});
// Read resources
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
switch (uri) {
case 'credit-scorer://methodology':
return {
contents: [
{
uri,
mimeType: 'text/markdown',
text: await import('fs').then(fs =>
fs.promises.readFile('../methodology-guide.md', 'utf-8')
),
},
],
};
case 'credit-scorer://scoring-tables':
return {
contents: [
{
uri,
mimeType: 'application/json',
text: await import('fs').then(fs =>
fs.promises.readFile('./data/scoring-tables.json', 'utf-8')
),
},
],
};
case 'credit-scorer://industry-data':
return {
contents: [
{
uri,
mimeType: 'application/json',
text: await import('fs').then(fs =>
fs.promises.readFile('./data/industry-data.json', 'utf-8')
),
},
],
};
default:
throw new Error(`Unknown resource: ${uri}`);
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
// Don't log to console in production - MCP uses stdio for communication
}
main().catch((error) => {
// In MCP, errors should be sent through the protocol, not to stderr
process.exit(1);
});