— Agent infrastructure · MCP

28 MCP tools for Norwegian business data

We ship a Model Context Protocol server alongside our REST API. The MCP server exposes 28 tools that an AI agent in Claude Desktop, Cursor, or any other MCP client can call to look up Norwegian companies, board members, procurement filings, ownership chains, and sanctions data. It is the same data as the REST API, but the surface looks different and the design constraints are different.

This post is what we learned designing it. We started with 41 tools and cut to 28. We added 3 back after watching real agent traces. The schema decisions that moved success rates most were not the ones we expected.

Tools shipped
28
down from 41
Schema rev count
14
in 6 weeks
Agent success rate
87%
on a 60-task eval
Median tools/task
2.3
down from 4.1

REST endpoints are not MCP tools

The first instinct, and the one we resisted only briefly before falling for it, is to expose each REST endpoint one-to-one as an MCP tool. Our REST API has 38 endpoints. So we started with 38 tools. This is wrong for two reasons.

First, REST endpoints are designed for humans reading documentation who can compose them. An agent reads the tool list once at session start, decides on a plan, and starts calling. There is no second pass to check the docs. If your tool name is /v1/companies/by-orgnr/{orgnr} the agent has to figure out from the schema description that this is "lookup a company by its organisation number", and sometimes it does not.

Second, the agent pays a context-window cost per tool. Each tool definition is roughly 200 to 800 tokens depending on schema complexity. Forty tools at 500 tokens each is 20 000 tokens of preamble that the agent reads before it sees the user is question. For Claude Desktop sessions where the user asks one quick question, this is most of the request.

What worked: collapse REST endpoints into purpose-shaped tools. The agent does not want get_company, get_company_address, get_company_status, get_company_employees. It wants lookup_company that returns everything relevant in one call, with a clear description of what it includes.

The 28 tools we ship

Grouped by domain. Names are exactly what an agent sees.

Discovery

search_companies
Free-text search by name, fragment, or org number. Returns top 25 matches with org number, official name, NACE code, city, status.
search_by_nace
List all active companies in a NACE industry code, optionally filtered by region, size, and revenue band. Paginated.
search_by_region
Companies registered in a Norwegian municipality or postal range. Useful for local-market analyses.
find_similar_companies
Given an org number, return up to 20 companies with similar NACE, size, and region. The agent's main "competitive set" tool.

Single-company lookup

lookup_company
Everything we know about one org number. Registry baseline plus enriched contacts plus latest financial summary if available. The single tool agents call most often.
get_company_contacts
Just the contacts. Used when the agent is already inside a workflow and wants the four-layer-enriched contact block without the rest of the payload.
get_company_history
Name changes, address changes, status transitions, signature-rights changes. The diff over time.

People and ownership

get_board_members
Current board members and roles for an org number, with birth year and signature rights.
get_signatories
Anyone with legal signing authority for the entity. Often overlaps with the board, not always.
get_ownership
Direct shareholders for an org number, with percentages where reported.
get_ownership_tree
Recursive ownership up to ultimate beneficial owners. Returns a tree, not a list.
find_companies_by_person
Given a name and birth year, list all companies where the person is on the board or holds shares.

Public-sector signal

search_tender_filings
Norwegian and EU-tier public procurement filings. Filter by buyer, supplier, NACE, date range, contract value.
get_contracts_won
All public contracts a specific org number has won, with values, customers, and contract type.
get_contracts_active
Contracts where the company is still the awarded supplier. Useful for "renewal" tracking.

Compliance

check_sanctions
Returns sanctions hits (EU, UN, OFAC) for a person or org. Used by KYB tools.
check_vat_registered
Boolean plus VAT number where applicable. Fastest tool we offer.
check_bankrupt
Active bankruptcy proceedings, with case number and court if applicable.

Financials

get_financial_summary
Headline financials for the last 5 reported years, where filed: revenue, operating result, employees, equity ratio.
get_financial_filings
Pointers to original officially-filed financial reports, with filing dates.

Aggregations and reporting

market_size_by_nace
Aggregate revenue, employee count, and company count for a NACE code, with optional region filter.
growth_companies
Companies with above-threshold revenue or headcount growth over the last two fiscal years.
new_registrations
Companies newly registered in a NACE code in a date range. Used for "spot new entrants in my market" prompts.
recently_dissolved
Same shape, opposite direction.

EU and R&D

search_eu_rnd_grants
Horizon Europe and EIC grants awarded to Norwegian entities. Filter by year, theme, value.
get_grant_recipients
Recipients of a specific grant programme, with awarded amounts.

Utility

normalise_orgnr
Cleans an input string into a 9-digit Norwegian org number. Validates check digit. Boring; agents use it constantly.
expand_nace_code
Maps a NACE code to its label, parent codes, and sibling codes. Helps the agent reason about industry adjacency.

The three tools we cut

We started with these and watched agents misuse them in production traces. They are gone now.

get_company_address was a one-field tool that returned just the address. Agents called it after lookup_company had already returned the address two messages earlier. The tool added a round trip with no marginal information. We merged it into lookup_company and saw the median tools-per-task drop almost in half.

get_company_emails returned a flat list of emails for an org number with no source attribution. Agents would happily forward this list to a user with the framing "here are John, Jane, and Anders' emails". We replaced it with get_company_contacts which always returns the role and confidence and source so the agent is forced to expose that to the user.

search_address was a reverse-geocoder against the company address column. It looked useful and was almost never called correctly. The agent would pass partial strings, get back an unsorted dump, and either give up or invent. We removed it. Anyone who wants reverse-address lookup can hit the REST endpoint.

Schema choices that moved the needle

Descriptions are the most-read text in your codebase

Every tool description gets read by the agent at the start of every session. If your description is "Returns the company", you have wasted the most important sentence you will write. We rewrote every description twice. The pattern that stuck:

{
  name: "lookup_company",
  description:
    "Get everything we know about one Norwegian company by its org number " +
    "(9 digits). Returns: registered name, address, NACE industry code and label, " +
    "active/dissolved status, founding year, employee count, VAT registration, " +
    "and up to 5 enriched executive contacts with name, role, email, phone. " +
    "Use this when the user names a specific company. For free-text search use " +
    "search_companies first.",
  ...
}

The "use this when... for X use Y instead" pattern at the end is the single biggest change. Agents pick tools by intent matching. Telling them when not to use a tool is as important as telling them when to use it.

Required vs optional matters more than in REST

An agent will not call a tool if it cannot fill all required parameters. We were too aggressive marking things required and watched the agent give up rather than make a reasonable default. Now nearly every filter parameter is optional with a documented default.

Return Markdown for human consumption, JSON for chaining

Some tools return both a structured JSON body and a Markdown rendering. The Markdown is what the agent quotes to the user; the JSON is what it uses for chained calls. This came from watching the agent paraphrase JSON responses badly. Pre-rendered Markdown is shorter to quote and avoids the model rewriting field names.

Pagination is your enemy

Agents page through results badly. They will call page=1, look at the first row, and stop. They will also occasionally call page=999 for no clear reason. We default to returning the top 25 of any list with a total_count field and a clear note in the description: "For paginated results past the first 25, the user should use the REST API." The MCP server is for interactive agent loops, not bulk extraction.

What we wish we had

Some agent behaviours we cannot yet support cleanly:

If you are designing your own

The summary:

  1. Start from intents, not endpoints. Ask "what is the agent trying to do" before "what data do I have".
  2. Cut aggressively. Fewer, better-described tools beat more tools every time. Twenty good tools is more capability than forty mediocre ones.
  3. Tell the agent when not to use a tool. The "for X, use Y" pattern in descriptions is unreasonably effective.
  4. Make filters optional with defaults. An agent that gives up because it cannot fill a required parameter looks broken to the user.
  5. Return human-readable and machine-readable side by side. Markdown for quoting, JSON for chaining.
  6. Watch real traces. The pattern you imagined the agent would use is rarely the one it actually does.

The Nordic Data MCP server is listed on Smithery and takes about thirty seconds to add to Claude Desktop or Cursor. The schemas are open. If you want to fork them, the names and descriptions are at the same URL.

Try the MCP server

Thirty-second setup in Claude Desktop or Cursor. Free tier, no card.

MCP setup instructions →