Skip to main content
Glama

Personupplysning MCP Server

bolagsverket-api.ts10.3 kB
/** * Bolagsverket API Client * OAuth2 + REST endpoints för företagsdata och dokument */ import axios, { AxiosInstance, AxiosError } from 'axios'; interface OAuth2Token { access_token: string; token_type: string; expires_in: number; scope: string; expires_at?: number; // Vi lägger till när token upphör } export interface BolagsverketDokument { dokumentId: string; filformat: string; rapporteringsperiodTom: string; registreringstidpunkt: string; } interface DokumentListaResponse { dokument: BolagsverketDokument[]; } interface BolagsverketConfig { clientId: string; clientSecret: string; tokenUrl: string; apiBaseUrl: string; maxRetries?: number; retryDelay?: number; enableLogging?: boolean; } export class BolagsverketAPIError extends Error { constructor( message: string, public statusCode?: number, public originalError?: any ) { super(message); this.name = 'BolagsverketAPIError'; } } export class BolagsverketClient { private config: BolagsverketConfig; private token: OAuth2Token | null = null; private httpClient: AxiosInstance; private readonly maxRetries: number; private readonly retryDelay: number; private readonly enableLogging: boolean; constructor(config?: Partial<BolagsverketConfig>) { this.config = { clientId: config?.clientId || process.env.BOLAGSVERKET_CLIENT_ID || '', clientSecret: config?.clientSecret || process.env.BOLAGSVERKET_CLIENT_SECRET || '', tokenUrl: 'https://portal.api.bolagsverket.se/oauth2/token', apiBaseUrl: 'https://gw.api.bolagsverket.se/vardefulla-datamangder/v1', maxRetries: 3, retryDelay: 1000, enableLogging: false, ...config, }; this.maxRetries = this.config.maxRetries!; this.retryDelay = this.config.retryDelay!; this.enableLogging = this.config.enableLogging!; if (!this.config.clientId || !this.config.clientSecret) { throw new BolagsverketAPIError( 'Missing Bolagsverket credentials. Set BOLAGSVERKET_CLIENT_ID and BOLAGSVERKET_CLIENT_SECRET in .env' ); } this.httpClient = axios.create({ baseURL: this.config.apiBaseUrl, timeout: 30000, }); } private log(message: string, data?: any): void { if (this.enableLogging) { console.log(`[BolagsverketClient] ${message}`, data || ''); } } private async sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } private isRetryableError(error: AxiosError): boolean { // Retry på nätverksfel eller 5xx server errors if (!error.response) return true; const status = error.response.status; return status >= 500 || status === 429; // 429 = Rate limit } /** * Hämta OAuth2 access token med retry-logik */ private async getAccessToken(): Promise<string> { // Kolla om vi har ett giltigt token if (this.token && this.token.expires_at && Date.now() < this.token.expires_at) { this.log('Using cached access token'); return this.token.access_token; } this.log('Fetching new access token'); // Annars hämta nytt token med retry const params = new URLSearchParams({ grant_type: 'client_credentials', scope: 'vardefulla-datamangder:ping vardefulla-datamangder:read', }); const auth = Buffer.from( `${this.config.clientId}:${this.config.clientSecret}` ).toString('base64'); let lastError: any; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { const response = await axios.post<OAuth2Token>( this.config.tokenUrl, params, { headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/x-www-form-urlencoded', }, } ); this.token = { ...response.data, expires_at: Date.now() + (response.data.expires_in * 1000) - 60000, // 1 min buffer }; this.log('Access token fetched successfully', { expires_in: response.data.expires_in, }); return this.token.access_token; } catch (error) { lastError = error; const axiosError = error as AxiosError; this.log(`Token fetch attempt ${attempt}/${this.maxRetries} failed`, { status: axiosError.response?.status, message: axiosError.message, }); if (attempt < this.maxRetries && this.isRetryableError(axiosError)) { const delay = this.retryDelay * Math.pow(2, attempt - 1); // Exponential backoff this.log(`Retrying in ${delay}ms`); await this.sleep(delay); } else { break; } } } throw new BolagsverketAPIError( 'Failed to get access token after retries', (lastError as AxiosError).response?.status, lastError ); } /** * Gör autentiserad API-förfrågan med retry-logik */ private async makeAuthenticatedRequest<T>( method: 'GET' | 'POST', endpoint: string, data?: any ): Promise<T> { let lastError: any; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { const token = await this.getAccessToken(); this.log(`${method} ${endpoint}`, data ? { dataKeys: Object.keys(data) } : undefined); const response = await this.httpClient.request<T>({ method, url: endpoint, data, headers: { 'Authorization': `Bearer ${token}`, 'Accept': '*/*', }, }); this.log(`${method} ${endpoint} succeeded`, { status: response.status, }); return response.data; } catch (error) { lastError = error; const axiosError = error as AxiosError; this.log(`${method} ${endpoint} attempt ${attempt}/${this.maxRetries} failed`, { status: axiosError.response?.status, message: axiosError.message, }); // Om token har löpt ut, rensa cache och försök igen if (axiosError.response?.status === 401) { this.log('Token expired, clearing cache'); this.token = null; } if (attempt < this.maxRetries && this.isRetryableError(axiosError)) { const delay = this.retryDelay * Math.pow(2, attempt - 1); // Exponential backoff this.log(`Retrying in ${delay}ms`); await this.sleep(delay); } else { break; } } } const axiosError = lastError as AxiosError; throw new BolagsverketAPIError( `${method} ${endpoint} failed after ${this.maxRetries} retries: ${axiosError.message}`, axiosError.response?.status, lastError ); } /** * Testa API-anslutningen */ async ping(): Promise<boolean> { try { await this.makeAuthenticatedRequest('GET', '/isalive'); this.log('Ping successful'); return true; } catch (error) { this.log('Ping failed', { error: (error as Error).message }); return false; } } /** * Sök organisationer * * @param criteria - Sökkriterier * @returns Företagsinformation */ async searchOrganizations(criteria: { identitetsbeteckning: string; // Organisationsnummer, personnummer eller samordningsnummer }): Promise<any> { return this.makeAuthenticatedRequest('POST', '/organisationer', criteria); } /** * Hämta dokumentlista för organisation * * @param organisationId - Organisations-ID (10 siffror) * @returns Lista av tillgängliga dokument */ async getDocumentList(organisationId: string): Promise<BolagsverketDokument[]> { const response = await this.makeAuthenticatedRequest<DokumentListaResponse>( 'POST', '/dokumentlista', { identitetsbeteckning: organisationId, } ); return response.dokument || []; } /** * Hämta specifikt dokument * * @param documentId - Dokument-ID från dokumentlista * @returns Dokument-innehåll som Buffer */ async getDocument(documentId: string): Promise<Buffer> { const token = await this.getAccessToken(); this.log(`GET /dokument/${documentId}`, { binary: true }); const response = await this.httpClient.request({ method: 'GET', url: `/dokument/${documentId}`, headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/zip', }, responseType: 'arraybuffer', }); this.log(`GET /dokument/${documentId} succeeded`, { status: response.status, size: response.data.byteLength, }); return Buffer.from(response.data); } /** * Hämta årsredovisning för företag * * @param organisationId - Organisations-ID * @param year - År (optional, senaste om inte angivet) * @returns Årsredovisning i iXBRL-format (ZIP-paket) */ async getAnnualReport(organisationId: string, year?: number): Promise<any> { const documentList = await this.getDocumentList(organisationId); if (documentList.length === 0) { throw new BolagsverketAPIError( `Inga dokument hittades för ${organisationId}` ); } // Sortera efter rapporteringsperiod, senaste först const sorted = [...documentList].sort((a, b) => new Date(b.rapporteringsperiodTom).getTime() - new Date(a.rapporteringsperiodTom).getTime() ); // Välj rätt år eller senaste const selected = year ? sorted.find((doc) => new Date(doc.rapporteringsperiodTom).getFullYear() === year ) : sorted[0]; if (!selected) { throw new BolagsverketAPIError( `Ingen årsredovisning för år ${year} hittades för ${organisationId}` ); } this.log('Fetching annual report', { dokumentId: selected.dokumentId, year: new Date(selected.rapporteringsperiodTom).getFullYear(), }); return this.getDocument(selected.dokumentId); } } // Singleton instance för enkel användning // Lazy initialization to allow dotenv to load first let _bolagsverketClient: BolagsverketClient | null = null; export const getBolagsverketClient = (): BolagsverketClient => { if (!_bolagsverketClient) { _bolagsverketClient = new BolagsverketClient(); } return _bolagsverketClient; };

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