Skip to main content
Glama
josuekongolo

CompanyIQ MCP Server

by josuekongolo
ssb.ts14.8 kB
import { config } from '../config.js'; export interface SSBTable { id: string; label: string; updated: string; firstPeriod: string; lastPeriod: string; variableNames: string[]; subjectCode: string; timeUnit: string; } export interface SSBTableMetadata { title: string; variables: { code: string; text: string; values: string[]; valueTexts: string[]; }[]; } export interface SSBDataQuery { query: { code: string; selection: { filter: string; values: string[]; }; }[]; response: { format: string; }; } export interface SSBTimeSeries { period: string; value: number; } export interface SSBTrendAnalysis { direction: 'increasing' | 'decreasing' | 'stable'; percentageChange: number; averageGrowth: number; periods: number; startValue: number; endValue: number; } export class SSBClient { private baseUrl = config.ssb.baseUrl; private username = config.ssb.username; private db: any; // Database instance for caching constructor(database?: any) { this.db = database; } private async fetch(endpoint: string, options: RequestInit = {}): Promise<any> { const url = `${this.baseUrl}${endpoint}`; console.error(`Fetching from SSB: ${url}`); const response = await fetch(url, { ...options, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': `CompanyIQ/${this.username}`, ...options.headers } }); if (!response.ok) { throw new Error(`SSB API error: ${response.status}`); } return await response.json(); } /** * Search for tables by keyword */ async searchTables(query: string, limit: number = 20): Promise<SSBTable[]> { try { const data = await this.fetch(`/tables?query=${encodeURIComponent(query)}&page=1&size=${limit}`); return data.tables || []; } catch (error) { console.error('Error searching SSB tables:', error); return []; } } /** * Get high-growth enterprises ("gaseller") by industry * Table 13706: High-growth firms by industry (NACE) */ async getHighGrowthEnterprises(naceCode?: string): Promise<any> { try { const tables = await this.searchTables('høgvekstføretak næring', 5); const table = tables.find(t => t.id === '13706' || t.id === '13758'); if (table) { return { tableId: table.id, label: table.label, period: `${table.firstPeriod}-${table.lastPeriod}`, description: 'High-growth enterprises and gazelles by industry sector' }; } return null; } catch (error) { console.error('Error fetching high-growth enterprises:', error); return null; } } /** * Get new enterprises statistics * Table 09028: New enterprises by industry (quarterly) */ async getNewEnterprises(): Promise<any> { try { const tables = await this.searchTables('nye foretak', 5); const table = tables.find(t => t.id === '09028'); if (table) { return { tableId: table.id, label: table.label, period: `${table.firstPeriod}-${table.lastPeriod}`, timeUnit: table.timeUnit, description: 'Newly established enterprises (quarterly updates)' }; } return null; } catch (error) { console.error('Error fetching new enterprises:', error); return null; } } /** * Get employment statistics by industry * Table 11656: Employment and wages by industry */ async getEmploymentByIndustry(): Promise<any> { try { const tables = await this.searchTables('sysselsatte næring', 10); const table = tables.find(t => t.id === '11656' || t.id === '13926'); if (table) { return { tableId: table.id, label: table.label, period: `${table.firstPeriod}-${table.lastPeriod}`, description: 'Employment and wages across industry sectors' }; } return null; } catch (error) { console.error('Error fetching employment data:', error); return null; } } /** * Get production and income statistics by industry * Table 09170: Production and income by industry */ async getProductionByIndustry(): Promise<any> { try { const tables = await this.searchTables('produksjon næring', 10); const table = tables.find(t => t.id === '09170'); if (table) { return { tableId: table.id, label: table.label, period: `${table.firstPeriod}-${table.lastPeriod}`, description: 'Production and income by industry sector (1970-2024)' }; } return null; } catch (error) { console.error('Error fetching production data:', error); return null; } } /** * Get innovation statistics by industry */ async getInnovationStats(): Promise<any> { try { const tables = await this.searchTables('innovasjon næring', 10); return tables.slice(0, 3).map(t => ({ tableId: t.id, label: t.label, period: `${t.firstPeriod}-${t.lastPeriod}` })); } catch (error) { console.error('Error fetching innovation stats:', error); return []; } } /** * Get regional industry statistics */ async getRegionalStats(region?: string): Promise<any> { try { const tables = await this.searchTables('regional næring', 10); return tables.slice(0, 3).map(t => ({ tableId: t.id, label: t.label, period: `${t.firstPeriod}-${t.lastPeriod}` })); } catch (error) { console.error('Error fetching regional stats:', error); return []; } } /** * Get comprehensive industry context */ async getIndustryContext(naceCode?: string): Promise<any> { try { const [highGrowth, employment, production, newEnterprises] = await Promise.all([ this.getHighGrowthEnterprises(naceCode), this.getEmploymentByIndustry(), this.getProductionByIndustry(), this.getNewEnterprises() ]); return { highGrowth, employment, production, newEnterprises, summary: 'SSB provides comprehensive statistics on Norwegian enterprises, industries, and economic indicators' }; } catch (error) { console.error('Error fetching industry context:', error); return null; } } /** * Get general economic indicators */ async getEconomicIndicators(): Promise<any> { try { const tables = await this.searchTables('økonomi', 10); return tables.slice(0, 5).map(t => ({ tableId: t.id, label: t.label, period: `${t.firstPeriod}-${t.lastPeriod}` })); } catch (error) { console.error('Error fetching economic indicators:', error); return []; } } /** * Get metadata for a specific table */ async getTableMetadata(tableId: string): Promise<SSBTableMetadata | null> { try { const data = await this.fetch(`/tables/${tableId}/metadata`); return { title: data.title, variables: data.variables?.map((v: any) => ({ code: v.code, text: v.text, values: v.values || [], valueTexts: v.valueTexts || [] })) || [] }; } catch (error) { console.error(`Error fetching metadata for table ${tableId}:`, error); return null; } } /** * Fetch actual data from an SSB table with filtering */ async fetchTableData( tableId: string, filters?: { naceCode?: string; region?: string; year?: string; limit?: number; } ): Promise<any> { try { // First get metadata to understand table structure const metadata = await this.getTableMetadata(tableId); if (!metadata) return null; // Build query based on filters const query: SSBDataQuery = { query: [], response: { format: 'json-stat2' } }; // Add filters for available dimensions metadata.variables.forEach(variable => { if (filters?.naceCode && variable.code.toLowerCase().includes('næring')) { query.query.push({ code: variable.code, selection: { filter: 'item', values: [filters.naceCode] } }); } else if (filters?.region && variable.code.toLowerCase().includes('region')) { query.query.push({ code: variable.code, selection: { filter: 'item', values: [filters.region] } }); } else if (filters?.year && variable.code.toLowerCase().includes('år')) { query.query.push({ code: variable.code, selection: { filter: 'item', values: [filters.year] } }); } else { // Include all values for unfiltered dimensions query.query.push({ code: variable.code, selection: { filter: 'item', values: variable.values.slice(0, filters?.limit || 10) } }); } }); // Fetch data const data = await this.fetch(`/tables/${tableId}/data`, { method: 'POST', body: JSON.stringify(query) }); return data; } catch (error) { console.error(`Error fetching data from table ${tableId}:`, error); return null; } } /** * Get time series data for trend analysis */ async getTimeSeries( tableId: string, filters?: { naceCode?: string; region?: string; } ): Promise<SSBTimeSeries[]> { try { const data = await this.fetchTableData(tableId, filters); if (!data || !data.value) return []; // Parse JSON-STAT2 format const dimension = data.dimension || {}; const timeVar = Object.keys(dimension).find(k => k.toLowerCase().includes('år') || k.toLowerCase().includes('tid') ); if (!timeVar) return []; const timeDimension = dimension[timeVar]; const periods = timeDimension.category?.index || []; const values = data.value || []; return Object.keys(periods).map((period, idx) => ({ period, value: values[idx] || 0 })); } catch (error) { console.error('Error getting time series:', error); return []; } } /** * Calculate growth trends from time series data */ analyzeTrend(timeSeries: SSBTimeSeries[]): SSBTrendAnalysis | null { if (timeSeries.length < 2) return null; const sorted = [...timeSeries].sort((a, b) => a.period.localeCompare(b.period)); const startValue = sorted[0].value; const endValue = sorted[sorted.length - 1].value; const totalChange = endValue - startValue; const percentageChange = (totalChange / startValue) * 100; // Calculate average period-over-period growth let totalGrowth = 0; for (let i = 1; i < sorted.length; i++) { const growth = ((sorted[i].value - sorted[i - 1].value) / sorted[i - 1].value) * 100; totalGrowth += growth; } const averageGrowth = totalGrowth / (sorted.length - 1); // Determine direction let direction: 'increasing' | 'decreasing' | 'stable'; if (percentageChange > 5) direction = 'increasing'; else if (percentageChange < -5) direction = 'decreasing'; else direction = 'stable'; return { direction, percentageChange: Math.round(percentageChange * 10) / 10, averageGrowth: Math.round(averageGrowth * 10) / 10, periods: sorted.length, startValue: Math.round(startValue), endValue: Math.round(endValue) }; } /** * Get high-growth enterprise data with actual numbers */ async getHighGrowthData(naceCode?: string, region?: string): Promise<any> { const tableId = '13706'; // High-growth firms by industry const filters = { naceCode, region }; // Check cache first if (this.db) { const cached = this.db.getSSBCache(tableId, filters); if (cached) { console.error(`SSB cache hit for table ${tableId}`); return { tableId, description: 'High-growth enterprises and gazelles by industry sector', period: cached.period, timeSeries: cached.time_series, trend: cached.trend_analysis, latestValue: cached.latest_value, cached: true }; } } try { const timeSeries = await this.getTimeSeries(tableId, { naceCode, region }); if (timeSeries.length === 0) { // Fallback to metadata only return await this.getHighGrowthEnterprises(naceCode); } const trend = this.analyzeTrend(timeSeries); const result = { tableId, description: 'High-growth enterprises and gazelles by industry sector', period: `${timeSeries[0]?.period}-${timeSeries[timeSeries.length - 1]?.period}`, timeSeries, trend, latestValue: timeSeries[timeSeries.length - 1]?.value }; // Cache the result if (this.db) { this.db.cacheSSBData({ table_id: tableId, table_name: 'High-growth enterprises', category: 'enterprise', filters, nace_code: naceCode, region, data: result, time_series: timeSeries, trend_analysis: trend }); } return result; } catch (error) { console.error('Error fetching high-growth data:', error); return await this.getHighGrowthEnterprises(naceCode); } } /** * Get employment data with trends */ async getEmploymentData(naceCode?: string, region?: string): Promise<any> { try { const tableId = '11656'; // Employment and wages by industry const timeSeries = await this.getTimeSeries(tableId, { naceCode, region }); if (timeSeries.length === 0) { return await this.getEmploymentByIndustry(); } const trend = this.analyzeTrend(timeSeries); return { tableId, description: 'Employment and wages across industry sectors', period: `${timeSeries[0]?.period}-${timeSeries[timeSeries.length - 1]?.period}`, timeSeries: timeSeries.slice(-5), // Last 5 periods trend, latestValue: timeSeries[timeSeries.length - 1]?.value, change: trend ? `${trend.direction === 'increasing' ? '+' : ''}${trend.percentageChange}%` : 'N/A' }; } catch (error) { console.error('Error fetching employment data:', error); return await this.getEmploymentByIndustry(); } } }

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