Skip to main content
Glama
josuekongolo

CompanyIQ MCP Server

by josuekongolo
db.ts21.2 kB
import pg from 'pg'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { config } from '../config.js'; const { Pool } = pg; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export interface CompanyRecord { org_nr: string; name: string; organization_form?: string; nace_code?: string; nace_description?: string; employees_count?: number; established_date?: string; business_address?: string; business_municipality?: string; bankrupt?: boolean; under_liquidation?: boolean; fetched_at?: string; [key: string]: any; } export class CompanyDatabase { private pool: pg.Pool; private initialized: boolean = false; constructor() { this.pool = new Pool({ connectionString: config.database.url, ssl: config.database.ssl ? { rejectUnauthorized: false } : false }); } async init(): Promise<void> { if (this.initialized) return; const schema = readFileSync(join(__dirname, 'schema.sql'), 'utf-8'); await this.pool.query(schema); this.initialized = true; console.log('Database initialized'); } // Expose pool for session store and auth services getPool(): pg.Pool { return this.pool; } // Company operations async insertOrUpdateCompany(company: CompanyRecord): Promise<void> { await this.pool.query(` INSERT INTO companies (org_nr, name, organization_form, organization_form_code, nace_code, nace_description, employees_count, established_date, business_address, business_postcode, business_city, business_municipality, business_municipality_number, postal_address, postal_postcode, postal_city, in_mva_register, in_foretaksregister, in_frivillighetsregister, in_stiftelsesregister, bankrupt, under_liquidation, under_forced_liquidation, last_updated, fetched_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, CURRENT_TIMESTAMP) ON CONFLICT (org_nr) DO UPDATE SET name = EXCLUDED.name, organization_form = EXCLUDED.organization_form, organization_form_code = EXCLUDED.organization_form_code, nace_code = EXCLUDED.nace_code, nace_description = EXCLUDED.nace_description, employees_count = EXCLUDED.employees_count, established_date = EXCLUDED.established_date, business_address = EXCLUDED.business_address, business_postcode = EXCLUDED.business_postcode, business_city = EXCLUDED.business_city, business_municipality = EXCLUDED.business_municipality, business_municipality_number = EXCLUDED.business_municipality_number, postal_address = EXCLUDED.postal_address, postal_postcode = EXCLUDED.postal_postcode, postal_city = EXCLUDED.postal_city, in_mva_register = EXCLUDED.in_mva_register, in_foretaksregister = EXCLUDED.in_foretaksregister, in_frivillighetsregister = EXCLUDED.in_frivillighetsregister, in_stiftelsesregister = EXCLUDED.in_stiftelsesregister, bankrupt = EXCLUDED.bankrupt, under_liquidation = EXCLUDED.under_liquidation, under_forced_liquidation = EXCLUDED.under_forced_liquidation, last_updated = EXCLUDED.last_updated, fetched_at = CURRENT_TIMESTAMP `, [ company.org_nr, company.name, company.organization_form, company.organization_form_code, company.nace_code, company.nace_description, company.employees_count, company.established_date, company.business_address, company.business_postcode, company.business_city, company.business_municipality, company.business_municipality_number, company.postal_address, company.postal_postcode, company.postal_city, company.in_mva_register ? true : false, company.in_foretaksregister ? true : false, company.in_frivillighetsregister ? true : false, company.in_stiftelsesregister ? true : false, company.bankrupt ? true : false, company.under_liquidation ? true : false, company.under_forced_liquidation ? true : false, company.last_updated ]); } async getCompany(orgNr: string): Promise<CompanyRecord | undefined> { const result = await this.pool.query('SELECT * FROM companies WHERE org_nr = $1', [orgNr]); return result.rows[0] as CompanyRecord | undefined; } async searchCompanies(filters: any): Promise<CompanyRecord[]> { const needsCEOFilter = filters.min_ceo_age || filters.max_ceo_age; let query = needsCEOFilter ? 'SELECT DISTINCT c.* FROM companies c INNER JOIN board_members b ON c.org_nr = b.company_org_nr WHERE b.role_type = \'DAGL\' AND b.is_active = true' : 'SELECT * FROM companies WHERE 1=1'; const params: any[] = []; let paramIndex = 1; if (filters.name) { query += needsCEOFilter ? ` AND c.name ILIKE $${paramIndex}` : ` AND name ILIKE $${paramIndex}`; params.push(`%${filters.name}%`); paramIndex++; } if (filters.nace_code) { query += needsCEOFilter ? ` AND c.nace_code LIKE $${paramIndex}` : ` AND nace_code LIKE $${paramIndex}`; params.push(`${filters.nace_code}%`); paramIndex++; } if (filters.municipality) { query += needsCEOFilter ? ` AND c.business_municipality ILIKE $${paramIndex}` : ` AND business_municipality ILIKE $${paramIndex}`; params.push(`%${filters.municipality}%`); paramIndex++; } if (filters.min_employees) { query += needsCEOFilter ? ` AND c.employees_count >= $${paramIndex}` : ` AND employees_count >= $${paramIndex}`; params.push(filters.min_employees); paramIndex++; } if (filters.max_employees) { query += needsCEOFilter ? ` AND c.employees_count <= $${paramIndex}` : ` AND employees_count <= $${paramIndex}`; params.push(filters.max_employees); paramIndex++; } if (filters.min_established_year) { const prefix = needsCEOFilter ? 'c.' : ''; query += ` AND (CAST(SUBSTRING(${prefix}established_date FROM 1 FOR 4) AS INTEGER) >= $${paramIndex} OR CAST(${prefix}established_date AS INTEGER) >= $${paramIndex + 1})`; params.push(filters.min_established_year, filters.min_established_year); paramIndex += 2; } if (filters.max_established_year) { const prefix = needsCEOFilter ? 'c.' : ''; query += ` AND (CAST(SUBSTRING(${prefix}established_date FROM 1 FOR 4) AS INTEGER) <= $${paramIndex} OR CAST(${prefix}established_date AS INTEGER) <= $${paramIndex + 1})`; params.push(filters.max_established_year, filters.max_established_year); paramIndex += 2; } // CEO age filtering if (filters.min_ceo_age) { const currentYear = new Date().getFullYear(); query += ` AND ( CASE WHEN LENGTH(b.birth_date) >= 8 THEN ${currentYear} - CAST(SUBSTRING(b.birth_date FROM 5 FOR 4) AS INTEGER) WHEN LENGTH(b.birth_date) = 6 THEN CASE WHEN CAST(SUBSTRING(b.birth_date FROM 5 FOR 2) AS INTEGER) <= ${currentYear % 100} THEN ${currentYear} - (2000 + CAST(SUBSTRING(b.birth_date FROM 5 FOR 2) AS INTEGER)) ELSE ${currentYear} - (1900 + CAST(SUBSTRING(b.birth_date FROM 5 FOR 2) AS INTEGER)) END ELSE NULL END ) >= $${paramIndex}`; params.push(filters.min_ceo_age); paramIndex++; } if (filters.max_ceo_age) { const currentYear = new Date().getFullYear(); query += ` AND ( CASE WHEN LENGTH(b.birth_date) >= 8 THEN ${currentYear} - CAST(SUBSTRING(b.birth_date FROM 5 FOR 4) AS INTEGER) WHEN LENGTH(b.birth_date) = 6 THEN CASE WHEN CAST(SUBSTRING(b.birth_date FROM 5 FOR 2) AS INTEGER) <= ${currentYear % 100} THEN ${currentYear} - (2000 + CAST(SUBSTRING(b.birth_date FROM 5 FOR 2) AS INTEGER)) ELSE ${currentYear} - (1900 + CAST(SUBSTRING(b.birth_date FROM 5 FOR 2) AS INTEGER)) END ELSE NULL END ) <= $${paramIndex}`; params.push(filters.max_ceo_age); paramIndex++; } if (filters.exclude_bankrupt) { query += needsCEOFilter ? ' AND (c.bankrupt IS NULL OR c.bankrupt = false)' : ' AND (bankrupt IS NULL OR bankrupt = false)'; } if (filters.in_foretaksregister) { query += needsCEOFilter ? ' AND c.in_foretaksregister = true' : ' AND in_foretaksregister = true'; } query += needsCEOFilter ? ` ORDER BY c.employees_count DESC NULLS LAST LIMIT $${paramIndex}` : ` ORDER BY employees_count DESC NULLS LAST LIMIT $${paramIndex}`; params.push(filters.limit || 100); const result = await this.pool.query(query, params); return result.rows as CompanyRecord[]; } // Board member operations async insertBoardMember(member: any): Promise<void> { await this.pool.query(` INSERT INTO board_members (company_org_nr, person_id, name, role_type, role_description, appointed_date, birth_date, is_active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT DO NOTHING `, [ member.company_org_nr, member.person_id, member.name, member.role_type, member.role_description, member.appointed_date, member.birth_date, member.is_active ? true : false ]); } async getBoardMembers(orgNr: string): Promise<any[]> { const result = await this.pool.query(` SELECT * FROM board_members WHERE company_org_nr = $1 AND is_active = true ORDER BY CASE role_type WHEN 'DAGL' THEN 1 WHEN 'LEDE' THEN 2 WHEN 'NEST' THEN 3 WHEN 'MEDL' THEN 4 ELSE 5 END `, [orgNr]); return result.rows; } async getCEO(orgNr: string): Promise<any> { const result = await this.pool.query(` SELECT * FROM board_members WHERE company_org_nr = $1 AND role_type = 'DAGL' AND is_active = true LIMIT 1 `, [orgNr]); return result.rows[0]; } calculateAge(birthDate: string): number | null { if (!birthDate) return null; const currentYear = new Date().getFullYear(); const currentYearTwoDigit = currentYear % 100; if (birthDate.length >= 8) { const year = parseInt(birthDate.substring(4, 8)); return currentYear - year; } else if (birthDate.length === 6) { const yearTwoDigit = parseInt(birthDate.substring(4, 6)); const year = yearTwoDigit <= currentYearTwoDigit ? 2000 + yearTwoDigit : 1900 + yearTwoDigit; return currentYear - year; } return null; } // Ownership operations async insertOwnership(ownership: any): Promise<void> { await this.pool.query(` INSERT INTO ownership (company_org_nr, owner_org_nr, owner_name, owner_type, ownership_percentage, shares_count) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT DO NOTHING `, [ ownership.company_org_nr, ownership.owner_org_nr, ownership.owner_name, ownership.owner_type, ownership.ownership_percentage, ownership.shares_count ]); } async getOwnership(orgNr: string): Promise<any[]> { const result = await this.pool.query(` SELECT * FROM ownership WHERE company_org_nr = $1 ORDER BY ownership_percentage DESC NULLS LAST `, [orgNr]); return result.rows; } // Subunits operations async insertSubunit(subunit: any): Promise<void> { await this.pool.query(` INSERT INTO subunits (org_nr, parent_org_nr, name, nace_code, employees_count, business_address, municipality, established_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (org_nr) DO UPDATE SET parent_org_nr = EXCLUDED.parent_org_nr, name = EXCLUDED.name, nace_code = EXCLUDED.nace_code, employees_count = EXCLUDED.employees_count, business_address = EXCLUDED.business_address, municipality = EXCLUDED.municipality, established_date = EXCLUDED.established_date, fetched_at = CURRENT_TIMESTAMP `, [ subunit.org_nr, subunit.parent_org_nr, subunit.name, subunit.nace_code, subunit.employees_count, subunit.business_address, subunit.municipality, subunit.established_date ]); } async getSubunits(parentOrgNr: string): Promise<any[]> { const result = await this.pool.query(` SELECT * FROM subunits WHERE parent_org_nr = $1 ORDER BY employees_count DESC NULLS LAST `, [parentOrgNr]); return result.rows; } // Financial snapshots async insertFinancialSnapshot(snapshot: any): Promise<void> { await this.pool.query(` INSERT INTO financial_snapshots (org_nr, year, revenue, profit, assets, equity, employees, source) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (org_nr, year) DO UPDATE SET revenue = EXCLUDED.revenue, profit = EXCLUDED.profit, assets = EXCLUDED.assets, equity = EXCLUDED.equity, employees = EXCLUDED.employees, source = EXCLUDED.source, fetched_at = CURRENT_TIMESTAMP `, [ snapshot.org_nr, snapshot.year, snapshot.revenue, snapshot.profit, snapshot.assets, snapshot.equity, snapshot.employees, snapshot.source ]); } async getFinancialHistory(orgNr: string, years: number = 5): Promise<any[]> { const result = await this.pool.query(` SELECT * FROM financial_snapshots WHERE org_nr = $1 ORDER BY year DESC LIMIT $2 `, [orgNr, years]); return result.rows; } // Growth analysis async getGrowthCompanies(naceCode?: string, region?: string, minGrowthPercent: number = 20, limit: number = 50): Promise<any[]> { let query = ` WITH latest_data AS ( SELECT f1.org_nr, f1.year as latest_year, f1.revenue as latest_revenue, f1.employees as latest_employees, f2.revenue as previous_revenue, f2.employees as previous_employees FROM financial_snapshots f1 LEFT JOIN financial_snapshots f2 ON f1.org_nr = f2.org_nr AND f2.year = f1.year - 1 WHERE f1.year = (SELECT MAX(year) FROM financial_snapshots WHERE org_nr = f1.org_nr) ) SELECT c.*, l.latest_revenue, l.latest_employees, l.previous_revenue, l.previous_employees, CAST(((l.latest_revenue - l.previous_revenue) * 100.0 / l.previous_revenue) AS INTEGER) as revenue_growth_percent, CAST(((l.latest_employees - l.previous_employees) * 100.0 / NULLIF(l.previous_employees, 0)) AS INTEGER) as employee_growth_percent FROM companies c JOIN latest_data l ON c.org_nr = l.org_nr WHERE l.previous_revenue > 0 AND ((l.latest_revenue - l.previous_revenue) * 100.0 / l.previous_revenue) >= $1 AND c.bankrupt = false `; const params: any[] = [minGrowthPercent]; let paramIndex = 2; if (naceCode) { query += ` AND c.nace_code LIKE $${paramIndex}`; params.push(`${naceCode}%`); paramIndex++; } if (region) { query += ` AND c.business_municipality ILIKE $${paramIndex}`; params.push(`%${region}%`); paramIndex++; } query += ` ORDER BY revenue_growth_percent DESC LIMIT $${paramIndex}`; params.push(limit); const result = await this.pool.query(query, params); return result.rows; } // Risk scoring async insertRiskScore(score: any): Promise<void> { await this.pool.query(` INSERT INTO risk_scores (org_nr, overall_score, bankruptcy_risk, growth_score, stability_score, financial_health_score, factors, calculated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, CURRENT_TIMESTAMP) ON CONFLICT (org_nr) DO UPDATE SET overall_score = EXCLUDED.overall_score, bankruptcy_risk = EXCLUDED.bankruptcy_risk, growth_score = EXCLUDED.growth_score, stability_score = EXCLUDED.stability_score, financial_health_score = EXCLUDED.financial_health_score, factors = EXCLUDED.factors, calculated_at = CURRENT_TIMESTAMP `, [ score.org_nr, score.overall_score, score.bankruptcy_risk, score.growth_score, score.stability_score, score.financial_health_score, JSON.stringify(score.factors) ]); } async getRiskScore(orgNr: string): Promise<any> { const result = await this.pool.query(` SELECT * FROM risk_scores WHERE org_nr = $1 `, [orgNr]); const row = result.rows[0]; if (row && row.factors && typeof row.factors === 'string') { row.factors = JSON.parse(row.factors); } return row; } // Market analysis cache async cacheMarketAnalysis(analysis: any): Promise<void> { await this.pool.query(` INSERT INTO market_analysis (nace_code, analysis_type, region, data, valid_until) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP + INTERVAL '${config.cache.ttlHours} hours') `, [ analysis.nace_code, analysis.analysis_type, analysis.region || '', JSON.stringify(analysis.data) ]); } async getMarketAnalysis(naceCode: string, analysisType: string, region?: string): Promise<any> { const result = await this.pool.query(` SELECT * FROM market_analysis WHERE nace_code = $1 AND analysis_type = $2 AND (region = $3 OR $4::text IS NULL) AND valid_until > CURRENT_TIMESTAMP ORDER BY created_at DESC LIMIT 1 `, [naceCode, analysisType, region || '', region || null]); const row = result.rows[0]; if (row && row.data && typeof row.data === 'string') { row.data = JSON.parse(row.data); } return row; } // SSB cache operations async cacheSSBData(cacheEntry: { table_id: string; table_name?: string; category?: string; filters?: any; nace_code?: string; region?: string; year?: string; data: any; metadata?: any; time_series?: any; trend_analysis?: any; }): Promise<void> { const filtersHash = cacheEntry.filters ? JSON.stringify(cacheEntry.filters).split('').reduce((a, b) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0).toString() : 'none'; await this.pool.query(` INSERT INTO ssb_cache (table_id, table_name, category, filters_hash, nace_code, region, year, data, metadata, time_series, trend_analysis, valid_until) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, CURRENT_TIMESTAMP + INTERVAL '${config.cache.ttlHours} hours') ON CONFLICT (table_id, filters_hash) DO UPDATE SET table_name = EXCLUDED.table_name, category = EXCLUDED.category, nace_code = EXCLUDED.nace_code, region = EXCLUDED.region, year = EXCLUDED.year, data = EXCLUDED.data, metadata = EXCLUDED.metadata, time_series = EXCLUDED.time_series, trend_analysis = EXCLUDED.trend_analysis, fetched_at = CURRENT_TIMESTAMP, valid_until = CURRENT_TIMESTAMP + INTERVAL '${config.cache.ttlHours} hours' `, [ cacheEntry.table_id, cacheEntry.table_name || null, cacheEntry.category || null, filtersHash, cacheEntry.nace_code || null, cacheEntry.region || null, cacheEntry.year || null, JSON.stringify(cacheEntry.data), cacheEntry.metadata ? JSON.stringify(cacheEntry.metadata) : null, cacheEntry.time_series ? JSON.stringify(cacheEntry.time_series) : null, cacheEntry.trend_analysis ? JSON.stringify(cacheEntry.trend_analysis) : null ]); } async getSSBCache(table_id: string, filters?: any): Promise<any> { const filtersHash = filters ? JSON.stringify(filters).split('').reduce((a: number, b: string) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0).toString() : 'none'; const result = await this.pool.query(` SELECT * FROM ssb_cache WHERE table_id = $1 AND filters_hash = $2 AND valid_until > CURRENT_TIMESTAMP ORDER BY fetched_at DESC LIMIT 1 `, [table_id, filtersHash]); const row = result.rows[0]; if (row) { return { ...row, data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data, metadata: row.metadata ? (typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata) : null, time_series: row.time_series ? (typeof row.time_series === 'string' ? JSON.parse(row.time_series) : row.time_series) : null, trend_analysis: row.trend_analysis ? (typeof row.trend_analysis === 'string' ? JSON.parse(row.trend_analysis) : row.trend_analysis) : null }; } return null; } async clearExpiredSSBCache(): Promise<void> { await this.pool.query(` DELETE FROM ssb_cache WHERE valid_until <= CURRENT_TIMESTAMP `); } async close(): Promise<void> { await this.pool.end(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/josuekongolo/companyiq-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server