#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { BrregAPIClient } from './client.js';
import {
BrregAPIError,
BrregValidationError,
BrregNotFoundError
} from './types.js';
import { NACEUtils } from './nace-utils.js';
// Tool definitions based on OpenAPI spec
const TOOLS: Tool[] = [
{
name: 'search_companies',
description: 'Search for companies in the Norwegian Business Registry with various filters',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Company name (free text, 1-180 characters)'
},
organizationNumber: {
type: 'array',
items: { type: 'string' },
description: 'Comma-separated list of organization numbers (9 digits)'
},
parentCompany: {
type: 'string',
description: 'Organization number of parent company in public sector'
},
fromEmployees: {
type: 'number',
description: 'Minimum number of employees (must be 0, 1, or >4)'
},
toEmployees: {
type: 'number',
description: 'Maximum number of employees (must be 0, 4, or >4)'
},
bankrupt: {
type: 'boolean',
description: 'Whether the company is bankrupt'
},
registeredInVAT: {
type: 'boolean',
description: 'Whether the company is registered in the VAT register'
},
registeredInBusinessRegister: {
type: 'boolean',
description: 'Whether the company is registered in the Business Register'
},
organizationForm: {
type: 'array',
items: { type: 'string' },
description: 'Comma-separated list of organization forms (e.g., AS, ENK)'
},
municipalityNumber: {
type: 'array',
items: { type: 'string' },
description: 'Comma-separated list of municipality numbers (4 digits)'
},
industryCode: {
type: 'array',
items: { type: 'string' },
description: 'Comma-separated list of industry codes (e.g., 41.109, 01.1). Supports hierarchical filtering - using 01.1 will include all subcategories like 01.11, 01.12, etc.'
},
hierarchicalIndustryFilter: {
type: 'boolean',
description: 'Enable hierarchical filtering for industry codes (default: true). When true, industry codes will automatically include all subcategories.',
default: true
},
page: {
type: 'number',
description: 'Page number (default: 1)',
default: 1
},
size: {
type: 'number',
description: 'Page size (default: 20)',
default: 20
},
sort: {
type: 'string',
description: 'Sort field and order (e.g., antallAnsatte,ASC or navn,DESC)'
}
}
}
},
{
name: 'get_company',
description: 'Get detailed information about a specific company by organization number',
inputSchema: {
type: 'object',
properties: {
organizationNumber: {
type: 'string',
description: 'Organization number (9 digits)'
}
},
required: ['organizationNumber']
}
},
{
name: 'get_company_roles',
description: 'Get all roles for a specific company',
inputSchema: {
type: 'object',
properties: {
organizationNumber: {
type: 'string',
description: 'Organization number (9 digits)'
}
},
required: ['organizationNumber']
}
},
{
name: 'search_subunits',
description: 'Search for subunits (business units) in the Norwegian Business Registry',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Subunit name (free text, 1-180 characters)'
},
organizationNumber: {
type: 'array',
items: { type: 'string' },
description: 'Comma-separated list of organization numbers'
},
parentCompany: {
type: 'string',
description: 'Parent company organization number'
},
fromEmployees: {
type: 'number',
description: 'Minimum number of employees'
},
toEmployees: {
type: 'number',
description: 'Maximum number of employees'
},
registeredInVAT: {
type: 'boolean',
description: 'Whether registered in VAT register'
},
page: {
type: 'number',
description: 'Page number (default: 1)',
default: 1
},
size: {
type: 'number',
description: 'Page size (default: 20)',
default: 20
}
}
}
},
{
name: 'get_services',
description: 'Get all available services from the Brreg API',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_subunit',
description: 'Get detailed information about a specific subunit by organization number',
inputSchema: {
type: 'object',
properties: {
organizationNumber: {
type: 'string',
description: 'Organization number (9 digits)'
}
},
required: ['organizationNumber']
}
},
{
name: 'get_organization_forms',
description: 'Get all organization forms',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number (default: 1)',
default: 1
},
size: {
type: 'number',
description: 'Page size (default: 20)',
default: 20
},
sort: {
type: 'string',
description: 'Sort field and order (e.g., kode,ASC or kode,DESC)'
}
}
}
},
{
name: 'get_organization_forms_for_units',
description: 'Get organization forms for main units',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number (default: 1)',
default: 1
},
size: {
type: 'number',
description: 'Page size (default: 20)',
default: 20
},
sort: {
type: 'string',
description: 'Sort field and order (e.g., kode,ASC or kode,DESC)'
}
}
}
},
{
name: 'get_organization_forms_for_subunits',
description: 'Get organization forms for subunits',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number (default: 1)',
default: 1
},
size: {
type: 'number',
description: 'Page size (default: 20)',
default: 20
},
sort: {
type: 'string',
description: 'Sort field and order (e.g., kode,ASC or kode,DESC)'
}
}
}
},
{
name: 'get_organization_form',
description: 'Get specific organization form by code',
inputSchema: {
type: 'object',
properties: {
organizationCode: {
type: 'string',
description: 'Organization form code (e.g., AS, ENK, ASA)'
}
},
required: ['organizationCode']
}
},
{
name: 'get_municipalities',
description: 'Get all municipalities',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number (default: 1)',
default: 1
},
size: {
type: 'number',
description: 'Page size (default: 20)',
default: 20
},
sort: {
type: 'string',
description: 'Sort field and order (e.g., navn,ASC or navn,DESC)'
}
}
}
},
{
name: 'get_municipality',
description: 'Get specific municipality by municipality number',
inputSchema: {
type: 'object',
properties: {
municipalityNumber: {
type: 'string',
description: 'Municipality number (4 digits)'
}
},
required: ['municipalityNumber']
}
},
{
name: 'get_role_types',
description: 'Get all role types',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_role_group_types',
description: 'Get all role group types',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_role_representatives',
description: 'Get all role representatives',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_company_updates',
description: 'Get updates for companies with optional filters',
inputSchema: {
type: 'object',
properties: {
date: {
type: 'string',
description: 'Show updates from and including this timestamp (ISO-8601: yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\')'
},
updatedBefore: {
type: 'string',
description: 'Show updates to and including this timestamp (ISO-8601: yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\')'
},
updateId: {
type: 'number',
description: 'Show only updates from and including update ID (>= 1)'
},
organizationNumber: {
type: 'array',
items: { type: 'string' },
description: 'Comma-separated list of organization numbers (9 digits each)'
},
includeChanges: {
type: 'boolean',
description: 'Include the changes that caused the update to be published'
},
page: {
type: 'number',
description: 'Page number (default: 1)',
default: 1
},
size: {
type: 'number',
description: 'Page size (default: 20, max: 10000)',
default: 20
},
sort: {
type: 'string',
description: 'Sort field and order (e.g., id,ASC or id,DESC). Only id is supported'
}
}
}
},
{
name: 'search_nace_codes',
description: 'Search NACE industry codes with hierarchical filtering support using the full SN2025 classification',
inputSchema: {
type: 'object',
properties: {
searchText: {
type: 'string',
description: 'Text to search for in NACE code names, short names, codes, or notes'
},
parentCode: {
type: 'string',
description: 'Get all child codes for a specific NACE parent code (e.g., "01.1" will return all subcategories)'
},
exactCode: {
type: 'string',
description: 'Get specific NACE code by exact code'
},
level: {
type: 'string',
description: 'Filter by NACE level (1-5). Level 1 = sections, Level 2 = divisions, Level 3 = groups, Level 4 = classes, Level 5 = national subcategories'
},
includeHierarchy: {
type: 'boolean',
description: 'Include full hierarchical path in results (default: true)',
default: true
},
includeNotes: {
type: 'boolean',
description: 'Include detailed notes in results (default: false)',
default: false
}
}
}
},
{
name: 'get_subunit_updates',
description: 'Get updates for subunits with optional filters',
inputSchema: {
type: 'object',
properties: {
date: {
type: 'string',
description: 'Show updates from and including this timestamp (ISO-8601: yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\')'
},
updatedBefore: {
type: 'string',
description: 'Show updates to and including this timestamp (ISO-8601: yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\')'
},
updateId: {
type: 'number',
description: 'Show only updates from and including update ID (>= 1)'
},
organizationNumber: {
type: 'array',
items: { type: 'string' },
description: 'Comma-separated list of organization numbers (9 digits each)'
},
includeChanges: {
type: 'boolean',
description: 'Include the changes that caused the update to be published'
},
page: {
type: 'number',
description: 'Page number (default: 1)',
default: 1
},
size: {
type: 'number',
description: 'Page size (default: 20, max: 10000)',
default: 20
},
sort: {
type: 'string',
description: 'Sort field and order (e.g., id,ASC or id,DESC). Only id is supported'
}
}
}
}
];
class BrregMCPServer {
private server: Server;
private client: BrregAPIClient;
constructor() {
this.server = new Server(
{
name: 'brreg-mcp-server',
version: '1.0.0',
},
);
this.client = new BrregAPIClient();
this.setupHandlers();
}
private setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: TOOLS,
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'search_companies':
return await this.searchCompanies(args);
case 'get_company':
return await this.getCompany(args);
case 'get_company_roles':
return await this.getCompanyRoles(args);
case 'search_subunits':
return await this.searchSubunits(args);
case 'get_services':
return await this.getServices();
case 'get_subunit':
return await this.getSubunit(args);
case 'get_organization_forms':
return await this.getOrganizationForms(args);
case 'get_organization_forms_for_units':
return await this.getOrganizationFormsForUnits(args);
case 'get_organization_forms_for_subunits':
return await this.getOrganizationFormsForSubunits(args);
case 'get_organization_form':
return await this.getOrganizationForm(args);
case 'get_municipalities':
return await this.getMunicipalities(args);
case 'get_municipality':
return await this.getMunicipality(args);
case 'get_role_types':
return await this.getRoleTypes();
case 'get_role_group_types':
return await this.getRoleGroupTypes();
case 'get_role_representatives':
return await this.getRoleRepresentatives();
case 'get_company_updates':
return await this.getCompanyUpdates(args);
case 'get_subunit_updates':
return await this.getSubunitUpdates(args);
case 'search_nace_codes':
return await this.searchNACECodes(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
let errorMessage = `Error: ${error instanceof Error ? error.message : String(error)}`;
// Add specific error handling for different error types
if (error instanceof BrregValidationError) {
errorMessage = `Validation Error: ${error.message}`;
} else if (error instanceof BrregNotFoundError) {
errorMessage = `Not Found: ${error.message}`;
} else if (error instanceof BrregAPIError) {
errorMessage = `API Error (${error.status}): ${error.message}`;
}
return {
content: [
{
type: 'text',
text: errorMessage,
},
],
isError: true,
};
}
});
}
private async searchCompanies(args: any) {
// Validate organization numbers if provided
if (args.organizationNumber) {
for (const orgNr of args.organizationNumber) {
if (!BrregAPIClient.validateOrganizationNumber(orgNr)) {
throw new BrregValidationError(`Invalid organization number format: ${orgNr}. Must be 9 digits.`);
}
}
}
// Validate municipality numbers if provided
if (args.municipalityNumber) {
for (const munNr of args.municipalityNumber) {
if (!BrregAPIClient.validateMunicipalityNumber(munNr)) {
throw new BrregValidationError(`Invalid municipality number format: ${munNr}. Must be 4 digits.`);
}
}
}
// Handle hierarchical industry code filtering
let industryCodes = args.industryCode;
if (industryCodes && args.hierarchicalIndustryFilter !== false) {
industryCodes = NACEUtils.expandIndustryCodes(industryCodes);
}
const params = this.client.buildParams({
navn: args.name,
organisasjonsnummer: args.organizationNumber,
overordnetEnhet: args.parentCompany,
fraAntallAnsatte: args.fromEmployees,
tilAntallAnsatte: args.toEmployees,
konkurs: args.bankrupt,
registrertIMvaregisteret: args.registeredInVAT,
registrertIForetaksregisteret: args.registeredInBusinessRegister,
organisasjonsform: args.organizationForm,
kommunenummer: args.municipalityNumber,
naeringskode: industryCodes,
page: args.page,
size: args.size,
sort: args.sort
});
const response = await this.client.get('/enhetsregisteret/api/enheter', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getCompany(args: any) {
const { organizationNumber } = args;
if (!BrregAPIClient.validateOrganizationNumber(organizationNumber)) {
throw new BrregValidationError(`Invalid organization number format: ${organizationNumber}. Must be 9 digits.`);
}
const response = await this.client.get(`/enhetsregisteret/api/enheter/${organizationNumber}`);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getCompanyRoles(args: any) {
const { organizationNumber } = args;
if (!BrregAPIClient.validateOrganizationNumber(organizationNumber)) {
throw new BrregValidationError(`Invalid organization number format: ${organizationNumber}. Must be 9 digits.`);
}
const response = await this.client.get(`/enhetsregisteret/api/enheter/${organizationNumber}/roller`);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async searchSubunits(args: any) {
// Validate organization numbers if provided
if (args.organizationNumber) {
for (const orgNr of args.organizationNumber) {
if (!BrregAPIClient.validateOrganizationNumber(orgNr)) {
throw new BrregValidationError(`Invalid organization number format: ${orgNr}. Must be 9 digits.`);
}
}
}
const params = this.client.buildParams({
navn: args.name,
organisasjonsnummer: args.organizationNumber,
overordnetEnhet: args.parentCompany,
fraAntallAnsatte: args.fromEmployees,
tilAntallAnsatte: args.toEmployees,
registrertIMvaregisteret: args.registeredInVAT,
page: args.page,
size: args.size
});
const response = await this.client.get('/enhetsregisteret/api/underenheter', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getServices() {
const response = await this.client.get('/enhetsregisteret/api');
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getSubunit(args: any) {
const { organizationNumber } = args;
if (!BrregAPIClient.validateOrganizationNumber(organizationNumber)) {
throw new BrregValidationError(`Invalid organization number format: ${organizationNumber}. Must be 9 digits.`);
}
const response = await this.client.get(`/enhetsregisteret/api/underenheter/${organizationNumber}`);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getOrganizationForms(args: any) {
const params = this.client.buildParams({
page: args.page,
size: args.size,
sort: args.sort
});
const response = await this.client.get('/enhetsregisteret/api/organisasjonsformer', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getOrganizationFormsForUnits(args: any) {
const params = this.client.buildParams({
page: args.page,
size: args.size,
sort: args.sort
});
const response = await this.client.get('/enhetsregisteret/api/organisasjonsformer/enheter', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getOrganizationFormsForSubunits(args: any) {
const params = this.client.buildParams({
page: args.page,
size: args.size,
sort: args.sort
});
const response = await this.client.get('/enhetsregisteret/api/organisasjonsformer/underenheter', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getOrganizationForm(args: any) {
const { organizationCode } = args;
const response = await this.client.get(`/enhetsregisteret/api/organisasjonsformer/${organizationCode}`);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getMunicipalities(args: any) {
const params = this.client.buildParams({
page: args.page,
size: args.size,
sort: args.sort
});
const response = await this.client.get('/enhetsregisteret/api/kommuner', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getMunicipality(args: any) {
const { municipalityNumber } = args;
if (!BrregAPIClient.validateMunicipalityNumber(municipalityNumber)) {
throw new BrregValidationError(`Invalid municipality number format: ${municipalityNumber}. Must be 4 digits.`);
}
const response = await this.client.get(`/enhetsregisteret/api/kommuner/${municipalityNumber}`);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getRoleTypes() {
const response = await this.client.get('/enhetsregisteret/api/roller/rolletyper');
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getRoleGroupTypes() {
const response = await this.client.get('/enhetsregisteret/api/roller/rollegruppetyper');
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getRoleRepresentatives() {
const response = await this.client.get('/enhetsregisteret/api/roller/representanter');
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getCompanyUpdates(args: any) {
// Validate organization numbers if provided
if (args.organizationNumber) {
for (const orgNr of args.organizationNumber) {
if (!BrregAPIClient.validateOrganizationNumber(orgNr)) {
throw new BrregValidationError(`Invalid organization number format: ${orgNr}. Must be 9 digits.`);
}
}
}
const params = this.client.buildParams({
dato: args.date,
updatedBefore: args.updatedBefore,
oppdateringsid: args.updateId,
organisasjonsnummer: args.organizationNumber,
includeChanges: args.includeChanges,
page: args.page,
size: args.size,
sort: args.sort
});
const response = await this.client.get('/enhetsregisteret/api/oppdateringer/enheter', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async getSubunitUpdates(args: any) {
// Validate organization numbers if provided
if (args.organizationNumber) {
for (const orgNr of args.organizationNumber) {
if (!BrregAPIClient.validateOrganizationNumber(orgNr)) {
throw new BrregValidationError(`Invalid organization number format: ${orgNr}. Must be 9 digits.`);
}
}
}
const params = this.client.buildParams({
dato: args.date,
updatedBefore: args.updatedBefore,
oppdateringsid: args.updateId,
organisasjonsnummer: args.organizationNumber,
includeChanges: args.includeChanges,
page: args.page,
size: args.size,
sort: args.sort
});
const response = await this.client.get('/enhetsregisteret/api/oppdateringer/underenheter', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
};
}
private async searchNACECodes(args: any) {
const { searchText, parentCode, exactCode, includeHierarchy = true, level } = args;
let results: any[] = [];
if (exactCode) {
const naceCode = NACEUtils.getNACEByCode(exactCode);
if (naceCode) {
results = [naceCode];
}
} else if (parentCode) {
results = NACEUtils.getHierarchicalCodes(parentCode);
} else if (level) {
results = NACEUtils.getNACECodesByLevel(level);
} else if (searchText) {
results = NACEUtils.searchNACECodes(searchText);
} else {
// If no specific search criteria, return all codes
results = NACEUtils.getAllNACECodes();
}
// Format results
const formattedResults = results.map(code => {
const result: any = {
code: code.code,
name: code.name,
shortName: code.shortName,
level: code.level,
parentCode: code.parentCode
};
if (includeHierarchy) {
result.fullCodePath = code.fullCodePath;
}
// Include notes if requested or if it's a single result
if (args.includeNotes || results.length === 1) {
result.notes = code.notes;
}
return result;
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
totalResults: formattedResults.length,
codes: formattedResults
}, null, 2),
},
],
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Brreg MCP server running on stdio');
}
}
const server = new BrregMCPServer();
server.run().catch(console.error);