Skip to main content
Glama

Personupplysning MCP Server

index-old.ts13.3 kB
#!/usr/bin/env node /** * Personupplysning MCP Server * * HTTP MCP server för svensk företags- och persondata * via Bolagsverket API och Supabase cache * * Deployment: Render (HTTP mode med SSE transport) * Local dev: Stdio mode */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { companyDataService } from './services/company-data-service.js'; import http from 'http'; import 'dotenv/config'; // Server info const SERVER_NAME = 'personupplysning-mcp'; const SERVER_VERSION = '0.1.0'; /** * MCP Tools Definition */ const TOOLS: Tool[] = [ { name: 'search_companies', description: 'Sök efter svenska företag i lokal databas (1.85M företag). Snabb sökning på företagsnamn eller organisationsnummer.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Sökterm: företagsnamn eller organisationsnummer (10 siffror)' }, limit: { type: 'number', description: 'Max antal resultat (default: 10)', default: 10 } }, required: ['query'] } }, { name: 'get_company_details', description: 'Hämta detaljerad företagsinformation från Bolagsverket API med cache-first strategi (30 dagars cache).', inputSchema: { type: 'object', properties: { organisationsidentitet: { type: 'string', description: 'Organisationsnummer (10 siffror)' } }, required: ['organisationsidentitet'] } }, { name: 'get_company_documents', description: 'Lista alla årsredovisningar och dokument för företag från Bolagsverket (7 dagars cache).', inputSchema: { type: 'object', properties: { organisationsidentitet: { type: 'string', description: 'Organisationsnummer (10 siffror)' } }, required: ['organisationsidentitet'] } }, { name: 'get_annual_report', description: 'Hämta och parsera årsredovisning för företag. Returnerar finansiell data extraherad från iXBRL-format.', inputSchema: { type: 'object', properties: { organisationsidentitet: { type: 'string', description: 'Organisationsnummer (10 siffror)' }, year: { type: 'number', description: 'År för årsredovisning (optional, senaste om ej angivet)' } }, required: ['organisationsidentitet'] } }, { name: 'get_cache_stats', description: 'Visa cache-statistik och API-användning för servern.', inputSchema: { type: 'object', properties: {} } } ]; /** * Create and configure MCP server */ function createServer(): Server { const server = new Server( { name: SERVER_NAME, version: SERVER_VERSION, }, { capabilities: { tools: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS, })); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'search_companies': { const { query, limit = 10 } = args as { query: string; limit?: number }; const results = await companyDataService.searchCompanies(query, limit); return { content: [ { type: 'text', text: JSON.stringify({ query, count: results.length, companies: results.map(c => ({ organisationsidentitet: c.organisationsidentitet, organisationsnamn: c.organisationsnamn, organisationsform: c.organisationsform, status: c.status, registreringsdatum: c.registreringsdatum })) }, null, 2) } ] }; } case 'get_company_details': { const { organisationsidentitet } = args as { organisationsidentitet: string }; const details = await companyDataService.getCompanyDetails(organisationsidentitet); if (!details) { return { content: [ { type: 'text', text: `Inget företag hittades med organisationsnummer: ${organisationsidentitet}` } ], isError: true }; } return { content: [ { type: 'text', text: JSON.stringify(details, null, 2) } ] }; } case 'get_company_documents': { const { organisationsidentitet } = args as { organisationsidentitet: string }; const documents = await companyDataService.getDocumentList(organisationsidentitet); return { content: [ { type: 'text', text: JSON.stringify({ organisationsidentitet, count: documents.length, documents: documents.map(d => ({ dokumentId: d.dokumentId, filformat: d.filformat, rapporteringsperiodTom: d.rapporteringsperiodTom, registreringstidpunkt: d.registreringstidpunkt, year: new Date(d.rapporteringsperiodTom).getFullYear() })) }, null, 2) } ] }; } case 'get_annual_report': { const { organisationsidentitet, year } = args as { organisationsidentitet: string; year?: number }; const report = await companyDataService.getAnnualReport(organisationsidentitet, year); return { content: [ { type: 'text', text: JSON.stringify({ organisationsidentitet, year: year || 'latest', storagePath: report.storagePath, financialData: report.data, note: 'Financial data parsing from iXBRL will be implemented in next phase' }, null, 2) } ] }; } case 'get_cache_stats': { const stats = await companyDataService.getCacheStats(); return { content: [ { type: 'text', text: JSON.stringify({ ...stats, server: { name: SERVER_NAME, version: SERVER_VERSION, uptime: process.uptime() } }, null, 2) } ] }; } default: return { content: [ { type: 'text', text: `Okänt verktyg: ${name}` } ], isError: true }; } } catch (error: any) { return { content: [ { type: 'text', text: `Error: ${error.message}\n\nStack: ${error.stack}` } ], isError: true }; } }); return server; } /** * Start server in HTTP mode (for Render deployment) */ async function startHTTPServer() { const PORT = parseInt(process.env.PORT || '3000'); const HOST = process.env.HOST || '0.0.0.0'; // Create MCP server once (not per request!) const mcpServer = createServer(); // Store transports by session ID const transports: Record<string, SSEServerTransport> = {}; const httpServer = http.createServer(async (req, res) => { // Health check endpoint if (req.url === '/health' || req.url === '/') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'healthy', server: SERVER_NAME, version: SERVER_VERSION, uptime: process.uptime(), endpoints: { sse: '/sse', messages: '/messages' }, environment: { SUPABASE_URL: process.env.SUPABASE_URL ? 'configured' : 'missing', SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY ? 'configured' : 'missing', BOLAGSVERKET_CLIENT_ID: process.env.BOLAGSVERKET_CLIENT_ID ? 'configured' : 'missing', BOLAGSVERKET_CLIENT_SECRET: process.env.BOLAGSVERKET_CLIENT_SECRET ? 'configured' : 'missing', } })); return; } // SSE endpoint - Creates transport and keeps connection open if (req.url === '/sse' && req.method === 'GET') { console.log('SSE connection established'); const transport = new SSEServerTransport('/messages', res); // Store transport by session ID transports[transport.sessionId] = transport; // Clean up on close res.on('close', () => { console.log(`SSE connection closed: ${transport.sessionId}`); delete transports[transport.sessionId]; }); // Connect server to transport await mcpServer.connect(transport); return; } // Messages endpoint - Handles incoming JSON-RPC messages if (req.url?.startsWith('/messages') && req.method === 'POST') { // Extract session ID from query parameter const url = new URL(req.url, `http://${req.headers.host}`); const sessionId = url.searchParams.get('sessionId'); if (!sessionId) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Missing sessionId parameter' })); return; } const transport = transports[sessionId]; if (!transport) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Session not found' })); return; } // Read request body let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', async () => { try { const message = JSON.parse(body); console.log('Received message:', message.method); await transport.handlePostMessage(req, res, message); } catch (error) { console.error('Error handling message:', error); if (!res.headersSent) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid request', message: error instanceof Error ? error.message : 'Unknown error' })); } } }); return; } // 404 for other routes res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found', endpoints: { health: '/', sse: '/sse', messages: '/messages?sessionId=<session-id>' } })); }); httpServer.listen(PORT, HOST, () => { console.log(`✓ ${SERVER_NAME} v${SERVER_VERSION} running on http://${HOST}:${PORT}`); console.log(`✓ Health check: http://${HOST}:${PORT}/health`); console.log(`✓ SSE endpoint: http://${HOST}:${PORT}/sse`); console.log(`✓ Messages endpoint: http://${HOST}:${PORT}/messages`); console.log('Environment:', { NODE_ENV: process.env.NODE_ENV || 'development', SUPABASE_URL: process.env.SUPABASE_URL ? '✓ configured' : '✗ missing', SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY ? '✓ configured' : '✗ missing', BOLAGSVERKET_CLIENT_ID: process.env.BOLAGSVERKET_CLIENT_ID ? '✓ configured' : '✗ missing', BOLAGSVERKET_CLIENT_SECRET: process.env.BOLAGSVERKET_CLIENT_SECRET ? '✓ configured' : '✗ missing', }); }); } /** * Start server in stdio mode (for local development) */ async function startStdioServer() { const server = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error(`${SERVER_NAME} v${SERVER_VERSION} running on stdio`); console.error('Environment:', { NODE_ENV: process.env.NODE_ENV, SUPABASE_URL: process.env.SUPABASE_URL ? '✓ configured' : '✗ missing', SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY ? '✓ configured' : '✗ missing', BOLAGSVERKET_CLIENT_ID: process.env.BOLAGSVERKET_CLIENT_ID ? '✓ configured' : '✗ missing', BOLAGSVERKET_CLIENT_SECRET: process.env.BOLAGSVERKET_CLIENT_SECRET ? '✓ configured' : '✗ missing', }); } /** * Main function - Start server based on mode */ async function main() { const mode = process.env.MCP_TRANSPORT || 'stdio'; if (mode === 'http') { await startHTTPServer(); } else { await startStdioServer(); } } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });

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/isakskogstad/personupplysning-mcp'

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