@heibergindustries/orakel-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @heibergindustries/orakel-mcp
2
+
3
+ MCP server for **Orakel** — Norwegian company data, financials, ownership, procurement, and prospecting tools. Works with Claude Code, Claude Desktop, and Gemini CLI.
4
+
5
+ ## Get Access
6
+
7
+ Request an API key at [orakel.heiberg.co](https://orakel.heiberg.co).
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ ORAKEL_API_KEY=orakel_YOUR_KEY npx -y @heibergindustries/orakel-mcp
13
+ ```
14
+
15
+ ## Setup — Claude Code
16
+
17
+ Add to `~/.claude/settings.json`:
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "orakel": {
23
+ "command": "npx",
24
+ "args": ["-y", "@heibergindustries/orakel-mcp"],
25
+ "env": {
26
+ "ORAKEL_API_KEY": "orakel_YOUR_KEY"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## Setup — Claude Desktop
34
+
35
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "orakel": {
41
+ "command": "npx",
42
+ "args": ["-y", "@heibergindustries/orakel-mcp"],
43
+ "env": {
44
+ "ORAKEL_API_KEY": "orakel_YOUR_KEY"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## Setup — Gemini CLI
52
+
53
+ Add to your Gemini CLI MCP config:
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "orakel": {
59
+ "command": "npx",
60
+ "args": ["-y", "@heibergindustries/orakel-mcp"],
61
+ "env": {
62
+ "ORAKEL_API_KEY": "orakel_YOUR_KEY"
63
+ }
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ ## Available Tools
70
+
71
+ | Tool | Description |
72
+ |------|-------------|
73
+ | `lookup_company` | Look up a company by org number |
74
+ | `search_companies` | Search by name, NACE, municipality, county, employees, revenue, tech stack, ad pixels |
75
+ | `get_financials` | Get financial data (revenue, profit, assets, equity, debt, ratios) |
76
+ | `find_prospects` | Find prospects by industry, location, size — formatted for sales |
77
+ | `get_corporate_group` | Get parent company + subsidiaries hierarchy |
78
+ | `get_shareholders` | Get shareholder register with ownership percentages |
79
+ | `get_ownership_network` | Reverse lookup: what does a company/person own? |
80
+ | `list_municipalities` | List Norwegian municipalities and county codes |
81
+ | `search_inspections` | Search Mattilsynet food safety inspections (smilefjes) |
82
+ | `search_licenses` | Search alcohol/tobacco licenses (TBR) |
83
+ | `search_procurement` | Search public procurement notices (Doffin) |
84
+ | `enrichment_status` | View domain enrichment pipeline results |
85
+ | `push_to_destination` | Push companies to CRM (Attio, webhooks) |
86
+ | `list_destinations` | List configured push destinations |
87
+ | `manage_destination` | Create/update/delete push destinations |
88
+ | `check_health` | Check API health, database status, sync progress |
89
+
90
+ ## Skills
91
+
92
+ Pre-built guided workflows are included in the `skills/` directory of this package. Copy them to your `.claude/skills/` directory:
93
+
94
+ ```bash
95
+ cp -r $(npm root -g)/@heibergindustries/orakel-mcp/skills/* ~/.claude/skills/
96
+ ```
97
+
98
+ Available skills: `/prospect`, `/company-deep-dive`, `/push-to-crm`, `/market-scan`, `/ownership-map`.
99
+
100
+ ## Environment Variables
101
+
102
+ | Variable | Default | Description |
103
+ |----------|---------|-------------|
104
+ | `ORAKEL_API_KEY` | (required) | Your Orakel API key |
105
+ | `ORAKEL_URL` | `https://orakel.heiberg.co` | API base URL (override for local dev) |
106
+
107
+ ## Examples
108
+
109
+ Once configured, ask from any project:
110
+
111
+ - "Look up Equinor" → `lookup_company`
112
+ - "Find 15 bars in Oslo with over 10M revenue" → `find_prospects`
113
+ - "What companies does Aker own?" → `get_ownership_network`
114
+ - "Show me IT consulting companies in Bergen" → `search_companies`
115
+ - "Push these prospects to my CRM" → `push_to_destination`
116
+
117
+ ## Data Sources
118
+
119
+ All data sourced from Norwegian public registries under NLOD 2.0:
120
+
121
+ - **Brønnøysundregistrene** — Company register, roles, financials
122
+ - **Mattilsynet** — Food safety inspections
123
+ - **Helsedirektoratet** — Alcohol/tobacco licenses
124
+ - **Doffin** — Public procurement notices
125
+ - **Aksjonærregisteret** — Shareholder data
126
+ - **SSB** — Municipality reference data
127
+ - **NORID** — Domain verification
128
+
129
+ ## License
130
+
131
+ MIT
package/dist/server.js ADDED
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ const ORAKEL_URL = process.env.ORAKEL_URL ?? "https://orakel.heiberg.co";
6
+ const ORAKEL_API_KEY = process.env.ORAKEL_API_KEY ?? "";
7
+ if (!ORAKEL_API_KEY) {
8
+ console.error("Error: ORAKEL_API_KEY environment variable is required.\n\n" +
9
+ "Request access at https://orakel.heiberg.co\n\n" +
10
+ "Then configure in Claude Code (~/.claude/settings.json):\n\n" +
11
+ ' "mcpServers": {\n' +
12
+ ' "orakel": {\n' +
13
+ ' "command": "npx",\n' +
14
+ ' "args": ["-y", "@heibergindustries/orakel-mcp"],\n' +
15
+ ' "env": { "ORAKEL_API_KEY": "orakel_YOUR_KEY" }\n' +
16
+ ' }\n' +
17
+ ' }\n');
18
+ process.exit(1);
19
+ }
20
+ async function orakelFetch(path, options) {
21
+ const url = `${ORAKEL_URL}${path}`;
22
+ const response = await fetch(url, {
23
+ ...options,
24
+ headers: {
25
+ Authorization: `Bearer ${ORAKEL_API_KEY}`,
26
+ "Content-Type": "application/json",
27
+ ...options?.headers,
28
+ },
29
+ });
30
+ if (!response.ok) {
31
+ const text = await response.text();
32
+ throw new Error(`Orakel API error ${response.status}: ${text}`);
33
+ }
34
+ return response.json();
35
+ }
36
+ const server = new McpServer({
37
+ name: "orakel",
38
+ version: "1.0.0",
39
+ });
40
+ // ─── lookup_company ───────────────────────────────────────────
41
+ server.tool("lookup_company", "Look up a Norwegian company by organization number. Returns full details including financials, roles, and sub-units.", { orgNumber: z.string().describe("9-digit Norwegian organization number") }, async ({ orgNumber }) => {
42
+ const company = await orakelFetch(`/api/companies/${orgNumber}`);
43
+ return { content: [{ type: "text", text: JSON.stringify(company, null, 2) }] };
44
+ });
45
+ // ─── search_companies ─────────────────────────────────────────
46
+ server.tool("search_companies", "Search Norwegian companies by name, industry (NACE code), county, municipality, employee count, revenue, and more. Returns paginated results.", {
47
+ query: z.string().optional().describe("Search by company name"),
48
+ nace: z.string().optional().describe("NACE industry code prefix (e.g. '56.3' for bars/restaurants)"),
49
+ orgForm: z.string().optional().describe("Organization form code (AS, ASA, ENK, etc.)"),
50
+ county: z.string().optional().describe("2-digit county code (e.g. '39' for Vestfold, '03' for Oslo). Use list_municipalities to find codes."),
51
+ municipality: z.string().optional().describe("4-digit municipality number (e.g. '0301' for Oslo, '3905' for Tønsberg)"),
52
+ minEmployees: z.number().optional().describe("Minimum employee count"),
53
+ maxEmployees: z.number().optional().describe("Maximum employee count"),
54
+ minRevenue: z.number().optional().describe("Minimum revenue (NOK)"),
55
+ maxRevenue: z.number().optional().describe("Maximum revenue (NOK)"),
56
+ isBankrupt: z.boolean().optional().describe("Filter by bankruptcy status"),
57
+ isInGroup: z.boolean().optional().describe("Filter by whether company is part of a group"),
58
+ hasParent: z.boolean().optional().describe("Filter by whether company has a parent organization"),
59
+ hasTechnology: z.string().optional().describe("Filter by detected technology (e.g. 'WordPress', 'Shopify', 'HubSpot')"),
60
+ hasAnyAdPixel: z.boolean().optional().describe("Filter by companies running digital ads (Meta Pixel, Google Ads, etc.)"),
61
+ sort: z.string().optional().describe("Sort by: name, employeeCount, foundingDate, updatedAt"),
62
+ order: z.string().optional().describe("Sort order: asc or desc"),
63
+ limit: z.number().optional().describe("Max results (default 20, max 100)"),
64
+ }, async (params) => {
65
+ const searchParams = new URLSearchParams();
66
+ if (params.query)
67
+ searchParams.set("q", params.query);
68
+ if (params.nace)
69
+ searchParams.set("nace", params.nace);
70
+ if (params.orgForm)
71
+ searchParams.set("orgForm", params.orgForm);
72
+ if (params.county)
73
+ searchParams.set("county", params.county);
74
+ if (params.municipality)
75
+ searchParams.set("municipality", params.municipality);
76
+ if (params.minEmployees)
77
+ searchParams.set("minEmployees", String(params.minEmployees));
78
+ if (params.maxEmployees)
79
+ searchParams.set("maxEmployees", String(params.maxEmployees));
80
+ if (params.minRevenue)
81
+ searchParams.set("minRevenue", String(params.minRevenue));
82
+ if (params.maxRevenue)
83
+ searchParams.set("maxRevenue", String(params.maxRevenue));
84
+ if (params.isBankrupt !== undefined)
85
+ searchParams.set("isBankrupt", String(params.isBankrupt));
86
+ if (params.isInGroup !== undefined)
87
+ searchParams.set("isInGroup", String(params.isInGroup));
88
+ if (params.hasParent !== undefined)
89
+ searchParams.set("hasParent", String(params.hasParent));
90
+ if (params.hasTechnology)
91
+ searchParams.set("hasTechnology", params.hasTechnology);
92
+ if (params.hasAnyAdPixel !== undefined)
93
+ searchParams.set("hasAnyAdPixel", String(params.hasAnyAdPixel));
94
+ if (params.sort)
95
+ searchParams.set("sort", params.sort);
96
+ if (params.order)
97
+ searchParams.set("order", params.order);
98
+ if (params.limit)
99
+ searchParams.set("limit", String(params.limit));
100
+ const result = await orakelFetch(`/api/companies?${searchParams}`);
101
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
102
+ });
103
+ // ─── get_financials ───────────────────────────────────────────
104
+ server.tool("get_financials", "Get financial data (revenue, profit, assets, equity, debt) for a Norwegian company.", { orgNumber: z.string().describe("9-digit Norwegian organization number") }, async ({ orgNumber }) => {
105
+ const company = await orakelFetch(`/api/companies/${orgNumber}`);
106
+ const financials = company.financials ?? [];
107
+ return { content: [{ type: "text", text: JSON.stringify(financials, null, 2) }] };
108
+ });
109
+ // ─── find_prospects ───────────────────────────────────────────
110
+ server.tool("find_prospects", "Find potential business prospects by industry, location, size, revenue, and technology. Returns a formatted list with financials and tech stack.", {
111
+ nace: z.string().optional().describe("NACE industry code prefix"),
112
+ county: z.string().optional().describe("2-digit county code (e.g. '39' for Vestfold). Use list_municipalities to find codes."),
113
+ municipality: z.string().optional().describe("4-digit municipality number"),
114
+ minEmployees: z.number().optional().describe("Minimum employees"),
115
+ maxEmployees: z.number().optional().describe("Maximum employees"),
116
+ minRevenue: z.number().optional().describe("Minimum revenue (NOK)"),
117
+ hasTechnology: z.string().optional().describe("Filter by detected technology (e.g. 'WordPress', 'Shopify')"),
118
+ hasAnyAdPixel: z.boolean().optional().describe("Only companies running digital ads"),
119
+ limit: z.number().optional().describe("Max results (default 20)"),
120
+ }, async (params) => {
121
+ const searchParams = new URLSearchParams();
122
+ if (params.nace)
123
+ searchParams.set("nace", params.nace);
124
+ if (params.county)
125
+ searchParams.set("county", params.county);
126
+ if (params.municipality)
127
+ searchParams.set("municipality", params.municipality);
128
+ if (params.minEmployees)
129
+ searchParams.set("minEmployees", String(params.minEmployees));
130
+ if (params.maxEmployees)
131
+ searchParams.set("maxEmployees", String(params.maxEmployees));
132
+ if (params.minRevenue)
133
+ searchParams.set("minRevenue", String(params.minRevenue));
134
+ if (params.hasTechnology)
135
+ searchParams.set("hasTechnology", params.hasTechnology);
136
+ if (params.hasAnyAdPixel !== undefined)
137
+ searchParams.set("hasAnyAdPixel", String(params.hasAnyAdPixel));
138
+ searchParams.set("limit", String(params.limit ?? 20));
139
+ searchParams.set("sort", "employeeCount");
140
+ searchParams.set("order", "desc");
141
+ const result = await orakelFetch(`/api/companies?${searchParams}`);
142
+ const companies = result.data ?? [];
143
+ const formatted = companies.map((c) => {
144
+ const financials = c.financials ?? [];
145
+ const latest = financials[0];
146
+ return {
147
+ orgNumber: c.orgNumber,
148
+ name: c.name,
149
+ employees: c.employeeCount,
150
+ city: c.businessAddressCity,
151
+ nace: c.naceCode1,
152
+ revenue: latest?.revenue,
153
+ netResult: latest?.netResult,
154
+ };
155
+ });
156
+ return { content: [{ type: "text", text: JSON.stringify(formatted, null, 2) }] };
157
+ });
158
+ // ─── list_municipalities ─────────────────────────────────────
159
+ server.tool("list_municipalities", "List Norwegian municipalities and counties with their codes. Use this to find the right county or municipality code for search queries. For example, find that Vestfold = county code '39', or that Tønsberg = municipality '3905'.", {
160
+ county: z.string().optional().describe("2-digit county code to filter by (e.g. '39' for Vestfold)"),
161
+ }, async (params) => {
162
+ const searchParams = new URLSearchParams();
163
+ if (params.county)
164
+ searchParams.set("county", params.county);
165
+ const result = await orakelFetch(`/api/municipalities?${searchParams}`);
166
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
167
+ });
168
+ // ─── push_to_destination ──────────────────────────────────────
169
+ server.tool("push_to_destination", "Push company data to a configured destination (Attio CRM, webhook, etc.).", {
170
+ destinationName: z.string().describe("Name of the destination (e.g. 'zero7-attio')"),
171
+ orgNumbers: z.array(z.string()).describe("Array of 9-digit org numbers to push"),
172
+ }, async ({ destinationName, orgNumbers }) => {
173
+ const result = await orakelFetch(`/api/push/${destinationName}`, {
174
+ method: "POST",
175
+ body: JSON.stringify({ orgNumbers }),
176
+ });
177
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
178
+ });
179
+ // ─── list_destinations ────────────────────────────────────────
180
+ server.tool("list_destinations", "List all configured push destinations (Attio, webhooks, etc.).", {}, async () => {
181
+ const destinations = await orakelFetch("/api/destinations");
182
+ return { content: [{ type: "text", text: JSON.stringify(destinations, null, 2) }] };
183
+ });
184
+ // ─── manage_destination ───────────────────────────────────────
185
+ server.tool("manage_destination", "Create, update, or delete a push destination.", {
186
+ action: z.enum(["create", "update", "delete"]).describe("Action to perform"),
187
+ name: z.string().describe("Destination name"),
188
+ type: z.string().optional().describe("Destination type (attio, webhook)"),
189
+ config: z.record(z.string(), z.unknown()).optional().describe("Configuration object"),
190
+ isActive: z.boolean().optional().describe("Whether the destination is active"),
191
+ }, async ({ action, name, type, config, isActive }) => {
192
+ let result;
193
+ switch (action) {
194
+ case "create":
195
+ result = await orakelFetch("/api/destinations", {
196
+ method: "POST",
197
+ body: JSON.stringify({ name, type, config: config ?? {} }),
198
+ });
199
+ break;
200
+ case "update":
201
+ result = await orakelFetch(`/api/destinations/${name}`, {
202
+ method: "PUT",
203
+ body: JSON.stringify({ type, config, isActive }),
204
+ });
205
+ break;
206
+ case "delete":
207
+ await orakelFetch(`/api/destinations/${name}`, { method: "DELETE" });
208
+ result = { deleted: name };
209
+ break;
210
+ }
211
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
212
+ });
213
+ // ─── search_inspections ──────────────────────────────────────
214
+ server.tool("search_inspections", "Search Mattilsynet food safety inspections (smilefjes). Filter by org number, rating (0=best, 3=worst), postal code, or date range.", {
215
+ orgNumber: z.string().optional().describe("Filter by 9-digit org number"),
216
+ minRating: z.number().optional().describe("Minimum overall rating (0=smiling, 3=very sad)"),
217
+ maxRating: z.number().optional().describe("Maximum overall rating"),
218
+ postnr: z.string().optional().describe("Postal code filter"),
219
+ fromDate: z.string().optional().describe("Inspections from this date (ISO format)"),
220
+ toDate: z.string().optional().describe("Inspections up to this date (ISO format)"),
221
+ limit: z.number().optional().describe("Max results (default 20)"),
222
+ }, async (params) => {
223
+ const searchParams = new URLSearchParams();
224
+ if (params.orgNumber)
225
+ searchParams.set("orgNumber", params.orgNumber);
226
+ if (params.minRating !== undefined)
227
+ searchParams.set("minRating", String(params.minRating));
228
+ if (params.maxRating !== undefined)
229
+ searchParams.set("maxRating", String(params.maxRating));
230
+ if (params.postnr)
231
+ searchParams.set("postnr", params.postnr);
232
+ if (params.fromDate)
233
+ searchParams.set("fromDate", params.fromDate);
234
+ if (params.toDate)
235
+ searchParams.set("toDate", params.toDate);
236
+ if (params.limit)
237
+ searchParams.set("limit", String(params.limit));
238
+ const result = await orakelFetch(`/api/inspections?${searchParams}`);
239
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
240
+ });
241
+ // ─── search_licenses ─────────────────────────────────────────
242
+ server.tool("search_licenses", "Search Norwegian alcohol and tobacco licenses (TBR). Filter by org number, license type, municipality, or active status.", {
243
+ orgNumber: z.string().optional().describe("Filter by org number"),
244
+ registerType: z.string().optional().describe("Alkoholsbevilling or Tobakksbevilling"),
245
+ licenseTypeCode: z.string().optional().describe("01SKJE (skjenke), 11ENGR (engros), 18TBIM (import), 19TBEK (export)"),
246
+ municipalityNo: z.string().optional().describe("Municipality number (e.g. '0301' for Oslo)"),
247
+ activeOnly: z.boolean().optional().describe("Only show currently valid licenses"),
248
+ limit: z.number().optional().describe("Max results (default 20)"),
249
+ }, async (params) => {
250
+ const searchParams = new URLSearchParams();
251
+ if (params.orgNumber)
252
+ searchParams.set("orgNumber", params.orgNumber);
253
+ if (params.registerType)
254
+ searchParams.set("registerType", params.registerType);
255
+ if (params.licenseTypeCode)
256
+ searchParams.set("licenseTypeCode", params.licenseTypeCode);
257
+ if (params.municipalityNo)
258
+ searchParams.set("municipalityNo", params.municipalityNo);
259
+ if (params.activeOnly !== undefined)
260
+ searchParams.set("activeOnly", String(params.activeOnly));
261
+ if (params.limit)
262
+ searchParams.set("limit", String(params.limit));
263
+ const result = await orakelFetch(`/api/licenses?${searchParams}`);
264
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
265
+ });
266
+ // ─── search_procurement ──────────────────────────────────────
267
+ server.tool("search_procurement", "Search Norwegian public procurement notices (Doffin). Filter by CPV code, contracting authority, value, status, or deadline.", {
268
+ query: z.string().optional().describe("Search in title"),
269
+ orgNumber: z.string().optional().describe("Contracting authority org number"),
270
+ cpvCode: z.string().optional().describe("CPV code (e.g. '72' for IT services)"),
271
+ status: z.string().optional().describe("Notice status: ACTIVE, EXPIRED"),
272
+ noticeType: z.string().optional().describe("Notice type: ANNOUNCEMENT_OF_COMPETITION, etc."),
273
+ minValue: z.number().optional().describe("Minimum estimated value (NOK)"),
274
+ deadlineAfter: z.string().optional().describe("Only notices with deadline after this date (ISO)"),
275
+ limit: z.number().optional().describe("Max results (default 20)"),
276
+ }, async (params) => {
277
+ const searchParams = new URLSearchParams();
278
+ if (params.query)
279
+ searchParams.set("q", params.query);
280
+ if (params.orgNumber)
281
+ searchParams.set("orgNumber", params.orgNumber);
282
+ if (params.cpvCode)
283
+ searchParams.set("cpvCode", params.cpvCode);
284
+ if (params.status)
285
+ searchParams.set("status", params.status);
286
+ if (params.noticeType)
287
+ searchParams.set("noticeType", params.noticeType);
288
+ if (params.minValue)
289
+ searchParams.set("minValue", String(params.minValue));
290
+ if (params.deadlineAfter)
291
+ searchParams.set("deadlineAfter", params.deadlineAfter);
292
+ if (params.limit)
293
+ searchParams.set("limit", String(params.limit));
294
+ const result = await orakelFetch(`/api/procurement?${searchParams}`);
295
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
296
+ });
297
+ // ─── enrichment_status ────────────────────────────────────────
298
+ server.tool("enrichment_status", "View domain enrichment results. Filter by status (confirmed, ambiguous, rejected) to review pipeline output. Shows company name, discovered domains, social handles, and confidence scores.", {
299
+ status: z.enum(["confirmed", "ambiguous", "rejected", "pending"]).optional().describe("Filter by enrichment status. Omit to see all non-pending results."),
300
+ minConfidence: z.number().optional().describe("Minimum confidence score (0-100)"),
301
+ limit: z.number().optional().describe("Max results (default 50, max 200)"),
302
+ }, async (params) => {
303
+ const searchParams = new URLSearchParams();
304
+ if (params.status)
305
+ searchParams.set("status", params.status);
306
+ if (params.minConfidence)
307
+ searchParams.set("minConfidence", String(params.minConfidence));
308
+ if (params.limit)
309
+ searchParams.set("limit", String(params.limit));
310
+ const result = await orakelFetch(`/api/enrichment?${searchParams}`);
311
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
312
+ });
313
+ // ─── get_corporate_group ─────────────────────────────────────
314
+ server.tool("get_corporate_group", "Get the corporate group tree for a company. Shows parent company and all subsidiaries in a hierarchy.", { orgNumber: z.string().describe("9-digit Norwegian organization number") }, async ({ orgNumber }) => {
315
+ const result = await orakelFetch(`/api/companies/${orgNumber}/group`);
316
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
317
+ });
318
+ // ─── get_shareholders ────────────────────────────────────────
319
+ server.tool("get_shareholders", "Get shareholders of a Norwegian company from the shareholder register (Aksjonærregisteret). Shows ownership percentages per share class.", {
320
+ orgNumber: z.string().describe("9-digit Norwegian organization number"),
321
+ year: z.number().optional().describe("Filter by snapshot year (e.g. 2024)"),
322
+ includePersons: z.boolean().optional().describe("Include individual (non-corporate) shareholders. Default: false for privacy."),
323
+ }, async ({ orgNumber, year, includePersons }) => {
324
+ const params = new URLSearchParams();
325
+ if (year)
326
+ params.set("year", String(year));
327
+ if (includePersons)
328
+ params.set("includePersons", "true");
329
+ const result = await orakelFetch(`/api/companies/${orgNumber}/shareholders?${params}`);
330
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
331
+ });
332
+ // ─── get_ownership_network ───────────────────────────────────
333
+ server.tool("get_ownership_network", "Find all companies where a given company is a shareholder (reverse ownership lookup). Answer: 'What does company X own?'", {
334
+ shareholderOrgNumber: z.string().describe("Org number of the shareholder company"),
335
+ year: z.number().optional().describe("Filter by snapshot year"),
336
+ }, async ({ shareholderOrgNumber, year }) => {
337
+ const params = new URLSearchParams({ shareholderOrgNumber });
338
+ if (year)
339
+ params.set("year", String(year));
340
+ const result = await orakelFetch(`/api/shareholders/by-owner?${params}`);
341
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
342
+ });
343
+ // ─── check_health ─────────────────────────────────────────────
344
+ server.tool("check_health", "Check Orakel API health, database status, record counts, and sync status for all data sources.", {}, async () => {
345
+ const health = await orakelFetch("/api/health");
346
+ return { content: [{ type: "text", text: JSON.stringify(health, null, 2) }] };
347
+ });
348
+ // ─── Start server ─────────────────────────────────────────────
349
+ const transport = new StdioServerTransport();
350
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@heibergindustries/orakel-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Orakel — Norwegian company data, financials, and prospecting tools for Claude Code, Claude Desktop & Gemini CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "orakel-mcp": "dist/server.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "skills"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc && chmod +x dist/server.js",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.29.0",
19
+ "zod": "^4.3.6"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.0.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/bendikheiberg/orakel",
31
+ "directory": "mcp"
32
+ },
33
+ "keywords": [
34
+ "mcp",
35
+ "model-context-protocol",
36
+ "claude",
37
+ "gemini",
38
+ "norway",
39
+ "company-data",
40
+ "business-intelligence"
41
+ ]
42
+ }
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: company-deep-dive
3
+ description: >
4
+ Research a single Norwegian company in depth. Use when asked to look up,
5
+ analyze, or investigate a specific company.
6
+ allowed-tools: mcp__orakel__lookup_company, mcp__orakel__get_financials, mcp__orakel__get_corporate_group, mcp__orakel__get_shareholders, mcp__orakel__get_ownership_network, mcp__orakel__search_companies, mcp__orakel__search_inspections, mcp__orakel__search_licenses
7
+ ---
8
+
9
+ # Company Deep Dive
10
+
11
+ Produce a comprehensive research report on a single Norwegian company.
12
+
13
+ ## Step 1: Identify the Company
14
+
15
+ The user may provide:
16
+
17
+ - **Organization number** (9 digits like 999888777): Use `lookup_company` directly.
18
+ - **Company name**: Use `search_companies` with the `query` parameter to find it.
19
+
20
+ If a name search returns multiple results, present the top matches and ask the user to confirm:
21
+
22
+ > I found several companies matching that name:
23
+ > 1. Example AS (999888777) - Oslo, IT consulting, 45 employees
24
+ > 2. Example Norge AS (888777666) - Bergen, retail, 12 employees
25
+ >
26
+ > Which one did you mean?
27
+
28
+ ## Step 2: Gather All Data
29
+
30
+ Once you have the org number, make these calls:
31
+
32
+ 1. `lookup_company` with the org number — this returns the core company data, roles, financials, sub-units, technologies, and enrichment data.
33
+ 2. `get_corporate_group` — to see parent company and subsidiaries.
34
+ 3. `get_shareholders` — to see ownership structure. Start without `includePersons` for privacy. Mention that personal shareholders can be included if needed.
35
+
36
+ If the company's NACE code starts with 55 or 56 (hospitality/food service), also run:
37
+ 4. `search_inspections` with the org number — Mattilsynet food safety ratings.
38
+ 5. `search_licenses` with the org number — alcohol/tobacco licenses.
39
+
40
+ ## Step 3: Produce the Report
41
+
42
+ Structure the report with these sections:
43
+
44
+ ### Overview
45
+ - Full legal name and any brand names
46
+ - Organization number
47
+ - Organization form (AS, ASA, ENK, etc.)
48
+ - Founding date and age
49
+ - Registered address and business address
50
+ - NACE industry code with plain-language description
51
+ - Number of employees
52
+ - Website and social media (from enrichment data if available)
53
+ - Technologies detected (if any)
54
+
55
+ ### Financial Performance
56
+
57
+ Present a multi-year table (most recent years first):
58
+
59
+ | Year | Revenue (MNOK) | Net Result (MNOK) | Profit Margin | Assets (MNOK) | Equity Ratio |
60
+ |------|---------------|-------------------|---------------|---------------|-------------|
61
+ | 2023 | 32.5 | 4.1 | 12.6% | 28.0 | 42% |
62
+ | 2022 | 28.9 | 3.2 | 11.1% | 24.5 | 38% |
63
+
64
+ Calculate and highlight:
65
+ - **Revenue trend:** Year-over-year growth percentage
66
+ - **Profit margin:** Net result / revenue (as percentage)
67
+ - **Equity ratio:** Equity / total assets (as percentage)
68
+
69
+ #### Interpreting the Numbers
70
+
71
+ Use these benchmarks to explain financial health in plain language:
72
+
73
+ | Metric | Healthy | Moderate | Risky |
74
+ |--------|---------|----------|-------|
75
+ | Profit margin | > 10% | 3-10% | < 3% or negative |
76
+ | Equity ratio | > 30% | 20-30% | < 20% |
77
+ | Revenue growth | > 10% | 0-10% | Declining |
78
+ | Debt-to-equity | < 2x | 2-4x | > 4x |
79
+
80
+ Examples of plain-language interpretation:
81
+ - "Revenue has grown 12% year-over-year, showing healthy expansion."
82
+ - "The equity ratio of 18% is below the 20% threshold, which means the company is heavily leveraged."
83
+ - "Profit margins have been declining for three consecutive years, from 15% to 8%."
84
+
85
+ ### Key People
86
+
87
+ List board members, CEO, and other roles from the company data. Format as:
88
+
89
+ - **Board Chair:** Name (since year)
90
+ - **CEO / Daglig leder:** Name (since year)
91
+ - **Board Members:** Name 1, Name 2, Name 3
92
+
93
+ ### Ownership Structure
94
+
95
+ From `get_shareholders`:
96
+ - List corporate shareholders with ownership percentages
97
+ - Note majority shareholders (> 50%) and significant holders (> 10%)
98
+ - If requested, include personal shareholders
99
+
100
+ ### Corporate Group
101
+
102
+ From `get_corporate_group`:
103
+ - Show parent company (if any)
104
+ - List subsidiaries (if any)
105
+ - Note the company's position in the hierarchy
106
+
107
+ ### Food Safety and Licenses (hospitality only)
108
+
109
+ If the company is in NACE 55-56:
110
+
111
+ **Inspections (Smilefjes):**
112
+ - Most recent inspection date and rating
113
+ - Rating scale: 0 = smiling face (best), 1 = neutral, 2 = sad, 3 = very sad (worst)
114
+ - Any recent downgrades or improvements
115
+
116
+ **Licenses:**
117
+ - Active alcohol/tobacco licenses with type and validity period
118
+ - License types: 01SKJE = serving license, 11ENGR = wholesale, 18TBIM = import, 19TBEK = export
119
+
120
+ ## Step 4: Closing Summary
121
+
122
+ End the report with:
123
+ - A one-paragraph plain-language summary of the company's health and position
124
+ - The org number for reference: "Org number: 999888777"
125
+ - Offer next steps: "Would you like me to check what this company owns, push it to your CRM, or compare it with competitors?"
@@ -0,0 +1,173 @@
1
+ ---
2
+ name: market-scan
3
+ description: >
4
+ Analyze a Norwegian market segment — size, top players, technology landscape,
5
+ procurement opportunities, regulatory status. Use for market research or
6
+ proposal preparation.
7
+ allowed-tools: mcp__orakel__search_companies, mcp__orakel__find_prospects, mcp__orakel__search_procurement, mcp__orakel__search_licenses, mcp__orakel__search_inspections, mcp__orakel__list_municipalities
8
+ ---
9
+
10
+ # Market Scan
11
+
12
+ Produce a market analysis of a Norwegian industry segment, suitable for proposals, strategy documents, or competitive intelligence.
13
+
14
+ ## Step 1: Define the Market Segment
15
+
16
+ Map the user's description to concrete filters. Ask clarifying questions if needed:
17
+
18
+ - **What industry?** Map to NACE code(s).
19
+ - **What geography?** Nationwide, specific county, or specific municipality.
20
+ - **What size range?** All companies, or only above a certain threshold.
21
+
22
+ ### NACE Industry Codes
23
+
24
+ | Code | Industry |
25
+ |------|----------|
26
+ | 01-03 | Agriculture, forestry, fishing |
27
+ | 10-12 | Food, beverage, and tobacco manufacturing |
28
+ | 41-43 | Construction (buildings, civil engineering, specialized) |
29
+ | 46 | Wholesale trade |
30
+ | 47 | Retail trade |
31
+ | 50 | Shipping / water transport |
32
+ | 55 | Hotels and accommodation |
33
+ | 56.1 | Restaurants and cafes |
34
+ | 56.3 | Bars and pubs |
35
+ | 62.01 | Software development |
36
+ | 62.02 | IT consulting |
37
+ | 62.09 | Other IT services |
38
+ | 64 | Financial services |
39
+ | 69.1 | Accounting and auditing |
40
+ | 69.2 | Legal services |
41
+ | 70.22 | Management consulting |
42
+ | 73.1 | Advertising agencies |
43
+ | 85 | Education |
44
+ | 86 | Health services |
45
+
46
+ ### County Codes
47
+
48
+ | Code | County |
49
+ |------|--------|
50
+ | 03 | Oslo |
51
+ | 11 | Rogaland |
52
+ | 15 | More og Romsdal |
53
+ | 18 | Nordland |
54
+ | 30 | Viken |
55
+ | 34 | Innlandet |
56
+ | 38 | Vestfold og Telemark |
57
+ | 42 | Agder |
58
+ | 46 | Vestland |
59
+ | 50 | Trondelag |
60
+ | 54 | Troms og Finnmark |
61
+
62
+ Use `list_municipalities` if the user names a specific city to find the municipality code.
63
+
64
+ ## Step 2: Market Overview
65
+
66
+ Run `search_companies` with the NACE and region filters. Set `limit: 100` and `sort: employeeCount`, `order: desc` to get the biggest players first.
67
+
68
+ Produce these metrics from the results:
69
+
70
+ ### Segment Size
71
+ - Total number of companies matching the criteria
72
+ - Total employees across the segment (sum of employeeCount)
73
+ - Estimated total revenue (sum of latest revenue from financials, where available)
74
+
75
+ ### Size Distribution
76
+ Categorize companies by employee count:
77
+
78
+ | Size Class | Employees | Count |
79
+ |------------|-----------|-------|
80
+ | Micro | 1-4 | |
81
+ | Small | 5-19 | |
82
+ | Medium | 20-99 | |
83
+ | Large | 100-249 | |
84
+ | Enterprise | 250+ | |
85
+
86
+ ### Top 10 by Revenue
87
+
88
+ | # | Company | Org Number | Location | Employees | Revenue (MNOK) |
89
+ |---|---------|-----------|----------|-----------|----------------|
90
+ | 1 | ... | ... | ... | ... | ... |
91
+
92
+ Format revenue in millions NOK (MNOK), rounded to one decimal.
93
+
94
+ ## Step 3: Technology Landscape
95
+
96
+ If tech stack data is available, summarize:
97
+ - Most common technologies detected (WordPress, Shopify, HubSpot, etc.)
98
+ - Percentage of companies running digital ads (hasAnyAdPixel)
99
+ - This reveals the digital maturity of the segment.
100
+
101
+ ## Step 4: Procurement Opportunities
102
+
103
+ Use `search_procurement` to find active public tenders relevant to the sector.
104
+
105
+ Map NACE codes to CPV codes for procurement search:
106
+
107
+ | NACE sector | CPV code prefix | Description |
108
+ |-------------|----------------|-------------|
109
+ | 62 (IT) | 72 | IT services |
110
+ | 41-43 (Construction) | 45 | Construction work |
111
+ | 85 (Education) | 80 | Education services |
112
+ | 86 (Health) | 85 | Health services |
113
+ | 73 (Advertising) | 79 | Business services |
114
+ | 55-56 (Hospitality) | 55 | Hotel and restaurant services |
115
+
116
+ Filter with `status: "ACTIVE"` and `deadlineAfter` set to today's date to show only open tenders.
117
+
118
+ Present as:
119
+
120
+ ### Active Tenders
121
+ | Title | Contracting Authority | Deadline | Est. Value (MNOK) |
122
+ |-------|-----------------------|----------|-------------------|
123
+
124
+ If no active tenders are found, say so explicitly — that's useful information too.
125
+
126
+ ## Step 5: Regulatory Landscape (Hospitality and Food Sectors Only)
127
+
128
+ For NACE 55-56 segments, add:
129
+
130
+ ### Food Safety Overview
131
+ Use `search_inspections` to get recent inspections in the region. Summarize:
132
+ - Distribution of ratings (how many 0/1/2/3)
133
+ - Average rating for the segment
134
+ - Rating scale: 0 = best (smiling face), 3 = worst (very sad face)
135
+
136
+ ### License Landscape
137
+ Use `search_licenses` to check the segment. Summarize:
138
+ - Number of active alcohol serving licenses (01SKJE)
139
+ - Number of wholesale licenses (11ENGR)
140
+ - Import/export licenses (18TBIM / 19TBEK)
141
+
142
+ ## Step 6: Format for Output
143
+
144
+ Structure the final report under clear headings. Use tables wherever possible. The output should be ready to copy-paste into a proposal or strategy document.
145
+
146
+ ### Suggested Report Structure
147
+
148
+ ```
149
+ # Market Analysis: [Industry] in [Region]
150
+
151
+ ## Executive Summary
152
+ [2-3 sentence overview of the market]
153
+
154
+ ## Market Size
155
+ [Segment size metrics and size distribution table]
156
+
157
+ ## Top Players
158
+ [Top 10 table by revenue]
159
+
160
+ ## Technology Adoption
161
+ [Tech landscape summary]
162
+
163
+ ## Public Procurement
164
+ [Active tenders or note that none exist]
165
+
166
+ ## Regulatory Landscape (if applicable)
167
+ [Inspection and license summary]
168
+
169
+ ## Key Takeaways
170
+ [3-5 bullet points summarizing the most important findings]
171
+ ```
172
+
173
+ If the user mentions this is for a specific proposal or client, adapt the framing accordingly.
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: ownership-map
3
+ description: >
4
+ Trace ownership networks and corporate hierarchies for Norwegian companies.
5
+ Use for due diligence, competitive intelligence, or compliance research.
6
+ allowed-tools: mcp__orakel__lookup_company, mcp__orakel__get_shareholders, mcp__orakel__get_ownership_network, mcp__orakel__get_corporate_group
7
+ ---
8
+
9
+ # Ownership Map
10
+
11
+ Trace ownership structures, corporate hierarchies, and shareholder networks for Norwegian companies.
12
+
13
+ ## Step 1: Identify the Target Company
14
+
15
+ The user may provide:
16
+
17
+ - **Organization number** (9 digits): Use `lookup_company` directly.
18
+ - **Company name**: Use `lookup_company` to search. If ambiguous, list the top matches and ask the user to confirm.
19
+
20
+ Get the basic company info first so you can reference the name and org number throughout the analysis.
21
+
22
+ ## Step 2: Gather Ownership Data
23
+
24
+ Make these calls:
25
+
26
+ 1. **`get_shareholders`** with the org number — who owns this company?
27
+ - Start with `includePersons: false` for privacy (only shows corporate shareholders).
28
+ - Mention that personal shareholders can be revealed if the user requests it.
29
+
30
+ 2. **`get_ownership_network`** with the org number as `shareholderOrgNumber` — what does this company own?
31
+ - This is the reverse lookup: shows companies where the target is a shareholder.
32
+
33
+ 3. **`get_corporate_group`** with the org number — formal corporate hierarchy.
34
+ - Shows the parent company and all subsidiaries registered in Brreg.
35
+ - This is the legal group structure, which may differ from the shareholder-based ownership.
36
+
37
+ ## Step 3: Build the Ownership Picture
38
+
39
+ ### 3a: Upward Ownership (Who Owns This Company?)
40
+
41
+ From `get_shareholders`, list all shareholders with their ownership percentage:
42
+
43
+ **Shareholders of [Company Name] (org: 999888777):**
44
+
45
+ | Shareholder | Org Number | Ownership % | Type |
46
+ |-------------|-----------|-------------|------|
47
+ | Parent Holdings AS | 888777666 | 67.0% | Majority |
48
+ | Investment Fund AS | 777666555 | 22.5% | Significant |
49
+ | Other shareholders | - | 10.5% | Minor |
50
+
51
+ Classification thresholds:
52
+ - **Majority:** > 50% ownership (controls the company)
53
+ - **Significant:** > 10% ownership (meaningful influence)
54
+ - **Minor:** < 10% ownership
55
+
56
+ ### 3b: Downward Ownership (What Does This Company Own?)
57
+
58
+ From `get_ownership_network`, list all companies where the target is a shareholder:
59
+
60
+ **Companies owned by [Company Name]:**
61
+
62
+ | Company | Org Number | Ownership % | Employees | Revenue (MNOK) |
63
+ |---------|-----------|-------------|-----------|----------------|
64
+ | Subsidiary One AS | 666555444 | 100% | 25 | 18.5 |
65
+ | Joint Venture AS | 555444333 | 50% | 8 | 5.2 |
66
+
67
+ ### 3c: Formal Corporate Group
68
+
69
+ From `get_corporate_group`, show the registered group hierarchy:
70
+
71
+ **Corporate group structure:**
72
+
73
+ ```
74
+ [Ultimate Parent] Holding Group AS (111222333)
75
+ |
76
+ +-- [Parent] Parent Holdings AS (888777666)
77
+ | |
78
+ | +-- [Target] Company Name AS (999888777) <-- target company
79
+ | |
80
+ | +-- Sibling Company AS (444333222)
81
+ |
82
+ +-- Other Branch AS (333222111)
83
+ ```
84
+
85
+ Note: The formal group structure (from Brreg) may differ from shareholder-based ownership. The group structure shows the registered parent-subsidiary relationships, while shareholders show the actual equity ownership.
86
+
87
+ ## Step 4: Trace One Level Deeper (if warranted)
88
+
89
+ For majority shareholders and significant holdings, offer to trace one level deeper:
90
+
91
+ > The majority shareholder is Parent Holdings AS (888777666). Want me to check who owns that company too?
92
+
93
+ **Important: Limit recursion to 2 levels maximum.** Beyond that, the network becomes unwieldy and may trigger many API calls. If the user wants to go deeper, do it one step at a time.
94
+
95
+ For each additional level, use `get_shareholders` on the parent company.
96
+
97
+ ## Step 5: Build a Text Tree Visualization
98
+
99
+ Combine all findings into a single ownership tree. Use indentation and lines to show relationships:
100
+
101
+ ```
102
+ Ownership Network for Company Name AS (999888777)
103
+ ==================================================
104
+
105
+ OWNERS (who owns this company):
106
+ [67.0%] Parent Holdings AS (888777666)
107
+ [100%] Ultimate Owner AS (111222333) <-- level 2
108
+ [22.5%] Investment Fund AS (777666555)
109
+ [10.5%] Other/personal shareholders
110
+
111
+ SUBSIDIARIES (what this company owns):
112
+ [100%] Subsidiary One AS (666555444) - 25 employees, 18.5 MNOK revenue
113
+ [ 50%] Joint Venture AS (555444333) - 8 employees, 5.2 MNOK revenue
114
+
115
+ CORPORATE GROUP (formal Brreg registration):
116
+ Ultimate Owner AS (111222333)
117
+ +-- Parent Holdings AS (888777666)
118
+ +-- ** Company Name AS (999888777) ** <-- target
119
+ +-- Sibling Company AS (444333222)
120
+ ```
121
+
122
+ ## Step 6: Flag Notable Patterns
123
+
124
+ Highlight any of these patterns if found:
125
+
126
+ - **Circular ownership:** Company A owns B, which owns A (or through intermediaries). This is unusual and worth flagging.
127
+ - **Shell company indicators:** A shareholder company with zero employees and zero revenue that exists solely as a holding entity. Not necessarily suspicious, but worth noting.
128
+ - **Same person/entity appearing multiple times:** If the same shareholder appears in multiple positions across the network, highlight this — it may indicate concentrated control.
129
+ - **Mismatch between group structure and ownership:** If the Brreg group tree and shareholder data tell different stories, note the discrepancy.
130
+ - **Foreign ownership:** If a shareholder is not found in Norwegian registries, it may be a foreign entity.
131
+ - **Recent changes:** If shareholder data shows different ownership across years, mention the change.
132
+
133
+ ## Step 7: Closing Summary
134
+
135
+ End with:
136
+
137
+ - A plain-language summary: "Company Name is majority-owned (67%) by Parent Holdings, which is in turn fully owned by Ultimate Owner. The company itself holds two subsidiaries."
138
+ - The target company's org number for reference.
139
+ - Offer next steps: "Want me to do a deep dive on any of these related companies, or check the financials across the group?"
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: prospect
3
+ description: >
4
+ Find and qualify Norwegian business prospects by industry, location, and size.
5
+ Use when looking for potential clients, leads, or market opportunities.
6
+ allowed-tools: mcp__orakel__search_companies, mcp__orakel__find_prospects, mcp__orakel__list_municipalities, mcp__orakel__get_financials, mcp__orakel__push_to_destination, mcp__orakel__list_destinations
7
+ ---
8
+
9
+ # Prospect Finder
10
+
11
+ Find and qualify Norwegian business prospects using Orakel's company database.
12
+
13
+ ## Step 1: Understand What the User is Looking For
14
+
15
+ Before searching, clarify these dimensions. If the user is vague, ask structured questions:
16
+
17
+ - **Industry:** What type of business? (restaurants, IT, construction, retail, etc.)
18
+ - **Region:** Where? (specific city, county, or all of Norway)
19
+ - **Size:** How big? (number of employees, revenue range)
20
+ - **Other filters:** Technology stack, ad spend, part of a corporate group?
21
+
22
+ Example prompt if the user says "find me some prospects":
23
+ > I can search Norwegian companies by industry, location, and size. Could you tell me:
24
+ > 1. What industry are you targeting?
25
+ > 2. Any geographic preference? (city, county, or nationwide)
26
+ > 3. What company size range? (employees or revenue)
27
+
28
+ ## Step 2: Map Request to Search Filters
29
+
30
+ Translate the user's plain-language request into API parameters using these reference tables.
31
+
32
+ ### NACE Industry Codes (use as prefix with `nace` parameter)
33
+
34
+ | Code | Industry |
35
+ |------|----------|
36
+ | 01 | Agriculture, crop production |
37
+ | 02 | Forestry |
38
+ | 03 | Fishing and aquaculture |
39
+ | 10 | Food manufacturing |
40
+ | 11 | Beverage manufacturing |
41
+ | 12 | Tobacco manufacturing |
42
+ | 41 | Building construction |
43
+ | 42 | Civil engineering |
44
+ | 43 | Specialized construction |
45
+ | 46 | Wholesale trade |
46
+ | 47 | Retail trade |
47
+ | 50 | Shipping / water transport |
48
+ | 55 | Hotels and accommodation |
49
+ | 56.1 | Restaurants and cafes |
50
+ | 56.3 | Bars and pubs |
51
+ | 62.01 | Software development |
52
+ | 62.02 | IT consulting |
53
+ | 62.09 | Other IT services |
54
+ | 64 | Financial services |
55
+ | 69.1 | Accounting and auditing |
56
+ | 69.2 | Legal services |
57
+ | 70.22 | Management consulting |
58
+ | 73.1 | Advertising agencies |
59
+ | 85 | Education |
60
+ | 86 | Health services |
61
+
62
+ Use partial codes for broader searches: `56` matches all food/drink, `62` matches all IT, `41` to `43` matches all construction.
63
+
64
+ ### County Codes (use with `county` parameter)
65
+
66
+ | Code | County |
67
+ |------|--------|
68
+ | 03 | Oslo |
69
+ | 11 | Rogaland |
70
+ | 15 | More og Romsdal |
71
+ | 18 | Nordland |
72
+ | 30 | Viken |
73
+ | 34 | Innlandet |
74
+ | 38 | Vestfold og Telemark |
75
+ | 42 | Agder |
76
+ | 46 | Vestland |
77
+ | 50 | Trondelag |
78
+ | 54 | Troms og Finnmark |
79
+
80
+ If the user names a city instead of a county, use `list_municipalities` to find the municipality code and use the `municipality` parameter instead.
81
+
82
+ ### Organization Forms
83
+
84
+ | Code | Type |
85
+ |------|------|
86
+ | AS | Private limited company (aksjeselskap) — most common |
87
+ | ASA | Public limited company |
88
+ | ENK | Sole proprietorship |
89
+ | NUF | Norwegian branch of foreign company |
90
+ | SA | Cooperative |
91
+
92
+ ## Step 3: Run the Search
93
+
94
+ Use `find_prospects` for prospect-oriented results (pre-sorted by size, includes financials).
95
+ Use `search_companies` for more flexible filtering (custom sort, more filter options).
96
+
97
+ Present results as a ranked summary table:
98
+
99
+ | # | Company | Org Number | Location | Employees | Revenue (MNOK) |
100
+ |---|---------|-----------|----------|-----------|----------------|
101
+ | 1 | Example AS | 999888777 | Oslo | 45 | 32.5 |
102
+
103
+ Format revenue in millions NOK (MNOK) for readability. Round to one decimal.
104
+
105
+ If no results are found:
106
+ - Try broadening the NACE code (use parent code like `56` instead of `56.1`)
107
+ - Try removing the county filter
108
+ - Try lowering the minimum employee/revenue threshold
109
+ - Suggest alternative NACE codes that might match what the user meant
110
+
111
+ ## Step 4: Offer Detailed Financials
112
+
113
+ After presenting the summary, offer:
114
+
115
+ > Would you like me to pull detailed financials for any of these companies? I can show multi-year revenue, profit, and key ratios.
116
+
117
+ Use `get_financials` for selected companies. Present year-over-year trends:
118
+
119
+ - Revenue trend (growing, stable, declining)
120
+ - Profit margin (net result / revenue)
121
+ - Whether they appear financially healthy
122
+
123
+ ### Financial Health Quick Guide
124
+
125
+ | Metric | Healthy | Caution | Warning |
126
+ |--------|---------|---------|---------|
127
+ | Profit margin | > 10% | 3-10% | < 3% or negative |
128
+ | Revenue growth YoY | > 5% | 0-5% | Declining |
129
+ | Equity ratio | > 30% | 20-30% | < 20% |
130
+
131
+ ## Step 5: Offer CRM Push
132
+
133
+ If the user seems satisfied with the results, offer:
134
+
135
+ > Want me to push these companies to your CRM? I can check which destinations are configured.
136
+
137
+ 1. Use `list_destinations` to show available CRM destinations.
138
+ 2. Confirm which companies to push (all results, or a selection).
139
+ 3. Use `push_to_destination` with the chosen destination name and org numbers.
140
+ 4. Report the results: how many were created, updated, or failed.
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: push-to-crm
3
+ description: >
4
+ Push company data to a configured CRM destination (Attio, HubSpot, webhooks).
5
+ Use when the user wants to export or sync companies to their CRM.
6
+ allowed-tools: mcp__orakel__list_destinations, mcp__orakel__push_to_destination, mcp__orakel__manage_destination, mcp__orakel__search_companies, mcp__orakel__lookup_company
7
+ ---
8
+
9
+ # Push to CRM
10
+
11
+ Export company data from Orakel to a configured CRM destination.
12
+
13
+ ## Step 1: Identify Which Companies to Push
14
+
15
+ The user might provide companies in several ways:
16
+
17
+ - **Explicit org numbers:** Use those directly.
18
+ - **Coming from a previous search or prospect list:** Offer to push all results or let the user select specific ones.
19
+ - **A company name:** Use `search_companies` or `lookup_company` to resolve the org number first.
20
+
21
+ If the user says something vague like "push those companies," refer back to the most recent search results in the conversation.
22
+
23
+ Confirm the list before pushing:
24
+
25
+ > I'll push these 5 companies to your CRM:
26
+ > 1. Example AS (999888777)
27
+ > 2. Another AS (888777666)
28
+ > 3. ...
29
+ >
30
+ > Which destination should I use?
31
+
32
+ ## Step 2: Check Available Destinations
33
+
34
+ Use `list_destinations` to show what's configured.
35
+
36
+ Present them simply:
37
+
38
+ > You have these CRM destinations set up:
39
+ > - **zero7-attio** (Attio CRM) - Active
40
+ > - **test-webhook** (Webhook) - Active
41
+
42
+ ### If No Destinations Exist
43
+
44
+ Walk the user through creating one:
45
+
46
+ > You don't have any CRM destinations configured yet. I can set one up for you.
47
+ > What type of destination do you want?
48
+ > - **Attio** — pushes companies as Attio records
49
+ > - **Webhook** — sends company data to a URL you specify
50
+
51
+ Then use `manage_destination` with action "create":
52
+
53
+ - For Attio: Need the Attio API key and workspace slug
54
+ - For Webhook: Need the target URL and optionally an auth header
55
+
56
+ Example:
57
+ ```
58
+ manage_destination(
59
+ action: "create",
60
+ name: "my-attio",
61
+ type: "attio",
62
+ config: { apiKey: "...", workspaceSlug: "..." }
63
+ )
64
+ ```
65
+
66
+ ### If a Destination is Inactive
67
+
68
+ Mention it and offer to reactivate:
69
+
70
+ > The destination "old-webhook" exists but is inactive. Want me to reactivate it?
71
+
72
+ Use `manage_destination` with action "update" and `isActive: true`.
73
+
74
+ ## Step 3: Execute the Push
75
+
76
+ Use `push_to_destination` with the destination name and array of org numbers.
77
+
78
+ **Important limits:**
79
+ - Push in batches if there are more than 50 companies (the API may time out on large batches).
80
+ - Each push is idempotent — pushing the same company twice will update rather than duplicate.
81
+
82
+ ## Step 4: Report Results
83
+
84
+ The push response includes counts of created, updated, and failed records. Report clearly:
85
+
86
+ > Push complete:
87
+ > - **Created:** 3 new records in Attio
88
+ > - **Updated:** 2 existing records refreshed
89
+ > - **Failed:** 0
90
+ >
91
+ > All 5 companies are now in your CRM.
92
+
93
+ If any failed, show the org numbers and error messages so the user can investigate.
94
+
95
+ ## Important Notes
96
+
97
+ - **Attio preserves brand names:** When pushing to Attio, company names from Orakel (legal names from Brreg) will not overwrite brand names that have been manually set in Attio. Only empty name fields get populated.
98
+ - **Data freshness:** Orakel pushes the latest data it has. Financial data may be up to a year old (depends on when the company filed).
99
+ - **Destination management:** Use `manage_destination` to create, update config, activate/deactivate, or delete destinations.