Skip to main content
Glama
josuekongolo

CompanyIQ MCP Server

by josuekongolo
http-server.ts19.2 kB
#!/usr/bin/env node /** * CompanyIQ MCP HTTP Server * * This wrapper exposes the MCP server over HTTP with: * - SSE (Server-Sent Events) transport for MCP protocol * - REST API endpoints for direct tool calls * - API key authentication * - Health check endpoint */ import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname, resolve, join } from 'path'; import express, { Request, Response, NextFunction } from 'express'; import cors from 'cors'; import session from 'express-session'; import pgSession from 'connect-pg-simple'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Load environment variables const envPaths = [ resolve(__dirname, '../.env'), resolve(__dirname, '.env'), resolve(process.cwd(), '.env'), ]; for (const envPath of envPaths) { const result = dotenv.config({ path: envPath }); if (!result.error) { console.log(`✅ Loaded .env from: ${envPath}`); break; } } import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { CompanyDatabase } from './database/db.js'; import { BrregClient } from './apis/brreg.js'; import { SSBClient } from './apis/ssb.js'; import { searchCompanies } from './tools/search_companies.js'; import { analyzeGrowth } from './tools/analyze_growth.js'; import { analyzeOwnership } from './tools/ownership_analysis.js'; import { trackBoard } from './tools/board_tracking.js'; import { analyzeFinancials } from './tools/financial_analysis.js'; import { analyzeMarketLandscape } from './tools/market_landscape.js'; import { analyzeConsolidation } from './tools/consolidation_trends.js'; import { getEconomicContext } from './tools/economic_context.js'; import { importFinancials, importFinancialsFromFile } from './tools/import_financials.js'; import { getCompany } from './tools/get_company.js'; import { getFinancialLink } from './tools/get_financial_link.js'; import { fetchFinancials } from './tools/fetch_financials.js'; import { searchBankruptCompanies } from './tools/search_bankrupt_companies.js'; import { buildFinancialHistory } from './tools/build_financial_history.js'; import { autoScrapeFinancials } from './tools/auto_scrape_financials.js'; import { createAuthRoutes } from './routes/auth-routes.js'; import { createApiKeyAuth } from './auth/middleware.js'; import { ApiKeyService } from './auth/api-key-service.js'; // Initialize API clients const db = new CompanyDatabase(); const brreg = new BrregClient(); const ssb = new SSBClient(db); // Database will be initialized asynchronously on server start // Tool definitions for listing const toolDefinitions = [ { name: "get_company", description: "Get complete information about a specific company by org number or name", inputSchema: { type: "object", properties: { org_nr: { type: "string", description: "Organization number (9 digits)" }, name: { type: "string", description: "Company name (if org_nr unknown)" } } } }, { name: "search_companies", description: "Search Norwegian companies by industry, size, region, establishment year, and CEO age", inputSchema: { type: "object", properties: { industry: { type: "string", description: "NACE code (e.g., '62' for IT)" }, name: { type: "string", description: "Company name or part of name" }, region: { type: "string", description: "Region/municipality" }, min_employees: { type: "number", description: "Minimum employees" }, max_employees: { type: "number", description: "Maximum employees" }, min_established_year: { type: "number", description: "Minimum establishment year" }, max_established_year: { type: "number", description: "Maximum establishment year" }, exclude_bankrupt: { type: "boolean", description: "Exclude bankrupt companies", default: true }, limit: { type: "number", description: "Max results", default: 50 } } } }, { name: "analyze_growth", description: "Identify high-growth companies in an industry or region", inputSchema: { type: "object", properties: { industry: { type: "string", description: "NACE code" }, region: { type: "string", description: "Region/county" }, min_growth_percent: { type: "number", description: "Minimum growth %", default: 20 }, time_period: { type: "string", enum: ["1_year", "3_years", "5_years"], default: "3_years" }, limit: { type: "number", description: "Number of results", default: 50 } } } }, { name: "analyze_ownership", description: "Analyze ownership structure and subsidiaries", inputSchema: { type: "object", properties: { org_nr: { type: "string", description: "Organization number (9 digits)" }, include_subunits: { type: "boolean", description: "Include subsidiaries", default: true }, depth: { type: "number", description: "Depth in ownership tree", default: 2 } }, required: ["org_nr"] } }, { name: "track_board", description: "Track board composition and leadership", inputSchema: { type: "object", properties: { org_nr: { type: "string", description: "Organization number" }, company_name: { type: "string", description: "Company name (alternative to org_nr)" } } } }, { name: "analyze_financials", description: "Financial analysis with automatic fetching of annual reports", inputSchema: { type: "object", properties: { org_nr: { type: "string", description: "Organization number" }, include_risk_assessment: { type: "boolean", description: "Include bankruptcy risk analysis", default: true }, auto_fetch: { type: "boolean", description: "Auto-fetch financial data if missing", default: true } }, required: ["org_nr"] } }, { name: "market_landscape", description: "Analyze competitive landscape in an industry", inputSchema: { type: "object", properties: { industry: { type: "string", description: "NACE code" }, region: { type: "string", description: "Region/county" }, include_stats: { type: "boolean", description: "Include statistics", default: true }, limit: { type: "number", description: "Max companies", default: 100 } }, required: ["industry"] } }, { name: "consolidation_trends", description: "Analyze consolidation trends and M&A activity", inputSchema: { type: "object", properties: { industry: { type: "string", description: "NACE code" }, time_period: { type: "string", enum: ["1_year", "3_years", "5_years"], default: "3_years" }, region: { type: "string", description: "Region/county" } }, required: ["industry"] } }, { name: "economic_context", description: "Get economic context and macro statistics from SSB", inputSchema: { type: "object", properties: { industry: { type: "string", description: "NACE code" }, region: { type: "string", description: "Region/county" }, include_innovation: { type: "boolean", description: "Include innovation stats", default: false } } } }, { name: "fetch_financials", description: "Fetch financial data from Brønnøysund Registry API", inputSchema: { type: "object", properties: { org_nr: { type: "string", description: "Organization number" }, auto_import: { type: "boolean", description: "Auto-save to database", default: true }, all_years: { type: "boolean", description: "Fetch all years", default: true } }, required: ["org_nr"] } }, { name: "get_financial_link", description: "Get direct link to download annual accounts from Brønnøysund", inputSchema: { type: "object", properties: { org_nr: { type: "string", description: "Organization number" }, year: { type: "number", description: "Accounting year" } }, required: ["org_nr"] } }, { name: "import_financials", description: "Manually import financial data for a company", inputSchema: { type: "object", properties: { org_nr: { type: "string", description: "Organization number" }, year: { type: "number", description: "Accounting year" }, revenue: { type: "number", description: "Revenue in NOK" }, profit: { type: "number", description: "Profit in NOK" }, assets: { type: "number", description: "Total assets in NOK" }, equity: { type: "number", description: "Total equity in NOK" }, employees: { type: "number", description: "Employee count" }, source: { type: "string", description: "Data source", default: "manual" } }, required: ["org_nr", "year"] } }, { name: "search_bankrupt_companies", description: "Search for bankrupt companies in an industry or region", inputSchema: { type: "object", properties: { industry: { type: "string", description: "NACE code" }, region: { type: "string", description: "Region/municipality" }, limit: { type: "number", description: "Max results", default: 50 } } } }, { name: "build_financial_history", description: "Build complete financial history for a company", inputSchema: { type: "object", properties: { org_nr: { type: "string", description: "Organization number" }, years_needed: { type: "number", description: "Total years desired", default: 5 } }, required: ["org_nr"] } }, { name: "auto_scrape_financials", description: "Automatically scrape all available annual reports using headless browser", inputSchema: { type: "object", properties: { org_nr: { type: "string", description: "Organization number" }, auto_import: { type: "boolean", description: "Auto-save to database", default: true }, use_api_first: { type: "boolean", description: "Try API first", default: true } }, required: ["org_nr"] } } ]; // Tool execution function async function executeTool(name: string, args: any): Promise<any> { switch (name) { case "get_company": return await getCompany(args, db, brreg); case "search_companies": return await searchCompanies(args, db, brreg); case "analyze_growth": return await analyzeGrowth(args, db, ssb); case "analyze_ownership": return await analyzeOwnership(args, db, brreg); case "track_board": return await trackBoard(args, db, brreg); case "analyze_financials": return await analyzeFinancials(args, db, brreg); case "market_landscape": return await analyzeMarketLandscape(args, db, brreg, ssb); case "consolidation_trends": return await analyzeConsolidation(args, db, ssb); case "economic_context": return await getEconomicContext(args, ssb); case "fetch_financials": return await fetchFinancials(args, db, brreg); case "get_financial_link": return await getFinancialLink(args, db, brreg); case "import_financials": return await importFinancials(args, db); case "import_financials_from_file": return await importFinancialsFromFile(args, db); case "search_bankrupt_companies": return await searchBankruptCompanies(args, db, brreg); case "build_financial_history": return await buildFinancialHistory(args, db, brreg); case "auto_scrape_financials": return await autoScrapeFinancials(args, db, brreg); default: throw new Error(`Unknown tool: ${name}`); } } // Create Express app const app = express(); // Middleware app.use(cors()); app.use(express.json()); // Session store setup (will be configured after db init) const PgStore = pgSession(session); // Serve static files for web portal app.use('/public', express.static(join(__dirname, 'public'))); // Session middleware (configured in startServer after db.init) let sessionMiddleware: express.RequestHandler; // Placeholder - will be replaced after db init app.use((req, res, next) => { if (sessionMiddleware) { return sessionMiddleware(req, res, next); } next(); }); // Health check endpoint app.get('/health', (_req: Request, res: Response) => { res.json({ status: 'healthy', service: 'companyiq-mcp', version: '1.0.0', timestamp: new Date().toISOString(), tools_available: toolDefinitions.length }); }); // Root endpoint - redirect to docs or show info app.get('/', (_req: Request, res: Response) => { res.redirect('/docs'); }); // Page routes for web portal app.get('/login', (_req: Request, res: Response) => { res.sendFile(join(__dirname, 'public/login.html')); }); app.get('/dashboard', (req: Request, res: Response) => { if (!req.session?.userId) { res.redirect('/login'); return; } res.sendFile(join(__dirname, 'public/dashboard.html')); }); app.get('/docs', (_req: Request, res: Response) => { res.sendFile(join(__dirname, 'public/docs.html')); }); // List available tools app.get('/api/tools', (_req: Request, res: Response) => { res.json({ tools: toolDefinitions }); }); // Execute a specific tool via REST API app.post('/api/tools/:toolName', async (req: Request, res: Response) => { const { toolName } = req.params; const args = req.body; try { console.log(`Executing tool via REST: ${toolName}`); const result = await executeTool(toolName, args); res.json(result); } catch (error) { console.error(`Tool execution error: ${error}`); const message = error instanceof Error ? error.message : String(error); res.status(400).json({ error: message, tool: toolName }); } }); // MCP Server instance for SSE function createMCPServer() { const mcpServer = new Server( { name: "companyiq-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); mcpServer.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: toolDefinitions }; }); mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; console.log(`Executing tool via MCP: ${name}`); return await executeTool(name, args); } catch (error) { console.error("Tool execution error:", error); const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true }; } }); return mcpServer; } // Store active SSE transports const transports: { [sessionId: string]: SSEServerTransport } = {}; // SSE endpoint for MCP protocol app.get('/sse', async (req: Request, res: Response) => { console.log('New SSE connection'); const transport = new SSEServerTransport('/messages', res); const sessionId = Date.now().toString(); transports[sessionId] = transport; const mcpServer = createMCPServer(); res.on('close', () => { console.log(`SSE connection closed: ${sessionId}`); delete transports[sessionId]; }); await mcpServer.connect(transport); }); // Messages endpoint for SSE transport app.post('/messages', async (req: Request, res: Response) => { const sessionId = req.query.sessionId as string; const transport = transports[sessionId]; if (!transport) { res.status(400).json({ error: 'No active SSE session' }); return; } try { await transport.handlePostMessage(req, res); } catch (error) { console.error('Message handling error:', error); res.status(500).json({ error: 'Failed to handle message' }); } }); // Error handling middleware app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { console.error('Unhandled error:', err); res.status(500).json({ error: 'Internal server error', message: err.message }); }); // Start server const PORT = process.env.PORT || 3000; const HOST = '0.0.0.0'; async function startServer() { try { // Initialize database (create tables if not exists) console.log('Initializing database...'); console.log(`DATABASE_URL set: ${process.env.DATABASE_URL ? 'Yes' : 'No'}`); await db.init(); console.log('Database initialized successfully'); // Configure session middleware now that db is ready sessionMiddleware = session({ store: new PgStore({ pool: db.getPool(), tableName: 'sessions', createTableIfMissing: true }), secret: process.env.SESSION_SECRET || 'companyiq-dev-secret-change-in-production', resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 24 * 60 * 60 * 1000 // 24 hours } }); // Add auth routes const pool = db.getPool(); app.use('/api/auth', createAuthRoutes(pool)); app.use('/api/admin', createAuthRoutes(pool)); // Add API key authentication for /api/tools routes const apiKeyService = new ApiKeyService(pool); app.use('/api/tools', createApiKeyAuth(apiKeyService)); app.use('/sse', createApiKeyAuth(apiKeyService)); console.log('Session and auth configured'); app.listen(Number(PORT), HOST, () => { console.log(` ╔═══════════════════════════════════════════════════════╗ ║ CompanyIQ MCP HTTP Server ║ ╠═══════════════════════════════════════════════════════╣ ║ Status: Running ║ ║ Port: ${String(PORT).padEnd(47)}║ ║ Tools: ${String(toolDefinitions.length).padEnd(46)}║ ╠═══════════════════════════════════════════════════════╣ ║ Endpoints: ║ ║ • GET /health - Health check ║ ║ • GET /login - Login page ║ ║ • GET /dashboard - Dashboard (auth required) ║ ║ • GET /docs - API documentation ║ ║ • GET /api/tools - List available tools ║ ║ • POST /api/tools/:n - Execute tool via REST ║ ║ • GET /sse - MCP SSE connection ║ ╚═══════════════════════════════════════════════════════╝ `); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } } startServer(); export { app };

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