#!/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 };