import express from 'express';
import bodyParser from 'body-parser';
import crypto from 'crypto';
import dotenv from 'dotenv';
import path from 'path';
import { RFP, ParsedRFP, QuoteEvaluationResult } from './types';
import { parseRFP } from './parser';
import { findSimilarQuotes, formatMatchesForReview } from './matcher';
import { estimateCostAndLeadTime, formatEstimateForReview } from './estimator';
import { generateQuote, formatQuoteDocument, formatQuoteEmail } from './generator';
import {
initializeStorage,
loadHistoricalQuotes,
saveHistoricalQuote,
saveEvaluation,
getEvaluation,
convertToHistoricalQuote,
} from './storage';
dotenv.config();
const app = express();
app.use(bodyParser.json());
// Serve static files from public directory
app.use(express.static(path.join(__dirname, '..', 'public')));
// Initialize storage on startup
initializeStorage();
// ==========================================
// MCP CAPABILITY FUNCTIONS
// ==========================================
/**
* Capability: ingestRfp
* Parses an RFP and extracts structured information
*/
app.post('/mcp/function/ingestRfp', async (req, res) => {
try {
const rfp: RFP = req.body;
if (!rfp.rawText) {
return res.status(400).json({ ok: false, error: 'rawText is required' });
}
const parsed = parseRFP(rfp);
return res.json({ ok: true, parsed });
} catch (error: any) {
return res.status(500).json({ ok: false, error: String(error) });
}
});
/**
* Capability: findSimilarQuotes
* Searches historical quotes for similar past work
*/
app.post('/mcp/function/findSimilarQuotes', async (req, res) => {
try {
const { parsedRfp } = req.body as { parsedRfp: ParsedRFP };
if (!parsedRfp) {
return res.status(400).json({ ok: false, error: 'parsedRfp is required' });
}
const historicalQuotes = loadHistoricalQuotes();
const matches = findSimilarQuotes(parsedRfp, historicalQuotes);
return res.json({ ok: true, matches });
} catch (error: any) {
return res.status(500).json({ ok: false, error: String(error) });
}
});
/**
* Capability: estimateCostLeadTime
* Calculates cost and lead time estimate
*/
app.post('/mcp/function/estimateCostLeadTime', async (req, res) => {
try {
const { parsedRfp, matches = [] } = req.body as any;
if (!parsedRfp) {
return res.status(400).json({ ok: false, error: 'parsedRfp is required' });
}
const estimate = estimateCostAndLeadTime(parsedRfp, matches);
return res.json({ ok: true, estimate });
} catch (error: any) {
return res.status(500).json({ ok: false, error: String(error) });
}
});
/**
* Capability: generateQuote
* Creates a formatted quote document
*/
app.post('/mcp/function/generateQuote', async (req, res) => {
try {
const { parsedRfp, estimate } = req.body as any;
if (!parsedRfp || !estimate) {
return res.status(400).json({
ok: false,
error: 'parsedRfp and estimate are required'
});
}
const doc = generateQuote(parsedRfp, estimate);
return res.json({ ok: true, doc });
} catch (error: any) {
return res.status(500).json({ ok: false, error: String(error) });
}
});
/**
* Capability: approveQuote
* Marks a quote as approved and optionally saves to historical database
*/
app.post('/mcp/function/approveQuote', async (req, res) => {
try {
const { quoteId, approvedBy, saveToHistory = true } = req.body as any;
if (!quoteId || !approvedBy) {
return res.status(400).json({
ok: false,
error: 'quoteId and approvedBy are required'
});
}
// In a real system, you'd update the quote in the database
// For now, we'll just return success
return res.json({
ok: true,
approved: true,
message: `Quote ${quoteId} approved by ${approvedBy}`
});
} catch (error: any) {
return res.status(500).json({ ok: false, error: String(error) });
}
});
/**
* Capability: sendQuote
* Sends quote via email (dry-run mode for safety)
*/
app.post('/mcp/function/sendQuote', async (req, res) => {
try {
const { doc, dryRun = true } = req.body as any;
if (!doc) {
return res.status(400).json({ ok: false, error: 'doc is required' });
}
const { subject, body } = formatQuoteEmail(doc);
if (dryRun) {
return res.json({
ok: true,
sent: false,
dryRun: true,
preview: {
to: doc.to,
subject,
body: body.substring(0, 500) + '...'
}
});
}
// In production, integrate with nodemailer or email service
// For now, just simulate success
return res.json({
ok: true,
sent: true,
message: `Quote sent to ${doc.to}`
});
} catch (error: any) {
return res.status(500).json({ ok: false, error: String(error) });
}
});
// ==========================================
// COORDINATOR FUNCTION
// ==========================================
/**
* Coordinator: evaluateRfpAndDraftQuote
* Orchestrates the entire quote generation pipeline
*/
app.post('/mcp/invoke/evaluateRfpAndDraftQuote', async (req, res) => {
try {
const { rfp, idempotencyKey: providedKey } = req.body as any;
if (!rfp) {
return res.status(400).json({ ok: false, error: 'rfp is required' });
}
// Generate or use provided idempotency key
const idempotencyKey = providedKey ||
crypto.createHash('sha256').update(JSON.stringify(rfp)).digest('hex');
// Check if we've already processed this request
const cached = getEvaluation(idempotencyKey);
if (cached) {
return res.json({ ok: true, cached: true, result: cached });
}
console.log('\n' + '='.repeat(70));
console.log('PROCESSING NEW RFP');
console.log('='.repeat(70));
// Step 1: Parse RFP
console.log('\n[1/4] Parsing RFP...');
const parsed = parseRFP(rfp);
console.log(` ✓ Extracted: ${parsed.material}, ${parsed.qty} units`);
console.log(` ✓ Processes: ${parsed.processes?.join(', ') || 'none specified'}`);
console.log(` ✓ Confidence: ${parsed.confidence}`);
// Step 2: Find similar quotes
console.log('\n[2/4] Searching historical quotes...');
const historicalQuotes = loadHistoricalQuotes();
const matches = findSimilarQuotes(parsed, historicalQuotes);
console.log(` ✓ Found ${matches.length} similar quotes`);
if (matches.length > 0) {
console.log(` ✓ Top match: ${matches[0].quote.id} (${(matches[0].score * 100).toFixed(1)}% similar)`);
}
// Step 3: Estimate cost and lead time
console.log('\n[3/4] Calculating cost estimate...');
const estimate = estimateCostAndLeadTime(parsed, matches);
console.log(` ✓ Total: $${estimate.totalPrice.toFixed(2)}`);
console.log(` ✓ Lead time: ${estimate.leadDays} days`);
console.log(` ✓ Confidence: ${estimate.confidence}`);
// Step 4: Generate quote document
console.log('\n[4/4] Generating quote document...');
const doc = generateQuote(parsed, estimate);
console.log(` ✓ Quote ID: ${doc.quoteId}`);
console.log(` ✓ Status: ${doc.status}`);
// Save evaluation
const result: QuoteEvaluationResult = {
parsedRfp: parsed,
matches,
estimate,
doc,
idempotencyKey,
};
saveEvaluation(result);
console.log('\n' + '='.repeat(70));
console.log('QUOTE GENERATION COMPLETE');
console.log('='.repeat(70) + '\n');
return res.json({ ok: true, result });
} catch (error: any) {
console.error('Error in coordinator:', error);
return res.status(500).json({ ok: false, error: String(error) });
}
});
// ==========================================
// UTILITY ENDPOINTS
// ==========================================
/**
* Get formatted review of a quote evaluation
*/
app.post('/mcp/utility/formatReview', async (req, res) => {
try {
const { result } = req.body as { result: QuoteEvaluationResult };
if (!result) {
return res.status(400).json({ ok: false, error: 'result is required' });
}
let review = '';
review += '\n' + '='.repeat(70) + '\n';
review += 'QUOTE EVALUATION REVIEW\n';
review += '='.repeat(70) + '\n\n';
review += 'PARSED RFP:\n';
review += ` Material: ${result.parsedRfp.material}\n`;
review += ` Quantity: ${result.parsedRfp.qty}\n`;
review += ` Processes: ${result.parsedRfp.processes?.join(', ') || 'none'}\n`;
review += ` Tolerances: ${result.parsedRfp.tolerances || 'not specified'}\n`;
review += ` Due Date: ${result.parsedRfp.dueDate || 'not specified'}\n`;
review += ` Confidence: ${result.parsedRfp.confidence}\n\n`;
review += formatMatchesForReview(result.matches) + '\n';
review += formatEstimateForReview(result.estimate) + '\n';
review += formatQuoteDocument(result.doc);
return res.json({ ok: true, review });
} catch (error: any) {
return res.status(500).json({ ok: false, error: String(error) });
}
});
/**
* Add a historical quote manually
*/
app.post('/mcp/utility/addHistoricalQuote', async (req, res) => {
try {
const quote = req.body;
saveHistoricalQuote(quote);
return res.json({ ok: true, message: 'Historical quote added' });
} catch (error: any) {
return res.status(500).json({ ok: false, error: String(error) });
}
});
/**
* Get all historical quotes
*/
app.get('/mcp/utility/historicalQuotes', async (req, res) => {
try {
const quotes = loadHistoricalQuotes();
return res.json({ ok: true, quotes, count: quotes.length });
} catch (error: any) {
return res.status(500).json({ ok: false, error: String(error) });
}
});
/**
* Health check
*/
app.get('/health', (req, res) => {
res.json({ ok: true, service: 'mcp-quoting-system' });
});
/**
* Root route - serve the web interface
*/
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
});
// ==========================================
// START SERVER
// ==========================================
const PORT = process.env.PORT || 3789;
app.listen(PORT, () => {
console.log('\n' + '='.repeat(70));
console.log('MCP QUOTING SYSTEM');
console.log('='.repeat(70));
console.log(`\n✓ Server running on http://localhost:${PORT}`);
console.log(`✓ Historical quotes loaded: ${loadHistoricalQuotes().length}`);
console.log('\nEndpoints:');
console.log(' POST /mcp/function/ingestRfp');
console.log(' POST /mcp/function/findSimilarQuotes');
console.log(' POST /mcp/function/estimateCostLeadTime');
console.log(' POST /mcp/function/generateQuote');
console.log(' POST /mcp/function/approveQuote');
console.log(' POST /mcp/function/sendQuote');
console.log(' POST /mcp/invoke/evaluateRfpAndDraftQuote (COORDINATOR)');
console.log('\nUtility:');
console.log(' POST /mcp/utility/formatReview');
console.log(' POST /mcp/utility/addHistoricalQuote');
console.log(' GET /mcp/utility/historicalQuotes');
console.log(' GET /health');
console.log('\n' + '='.repeat(70) + '\n');
});