Skip to main content
Glama
Quantum3-tech

companies-house-mcp

companies-house-mcp

An MCP server that lets Claude look up UK companies: search by name, then pull a company's profile, officers or filing history by number. It talks to the Companies House Public Data API over stdio.

I built it because the interesting problem is not the HTTP. The API is public and the calls are trivial. The problem is the contract: deciding what the model should be able to ask for, what comes back, and how it learns that something went wrong. That is where almost all of the thinking went, and it is what the rest of this file is about. If you want the reasoning in full, with the options I turned down, it is in DECISIONS.md.

Setup

You need a free API key. Register an application at https://developer.company-information.service.gov.uk/, create a key for the "Public Data API", and copy it.

Then:

npm install
npm run build

Point Claude Desktop at the built server by adding this to its config file (%APPDATA%\Claude\claude_desktop_config.json on Windows, ~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

{
  "mcpServers": {
    "companies-house": {
      "command": "node",
      "args": ["C:\\path\\to\\companies-house-mcp\\dist\\index.js"],
      "env": { "COMPANIES_HOUSE_API_KEY": "your-key-here" }
    }
  }
}

The key is read from the environment and nowhere else. If it is missing the server refuses to start and says why, rather than failing on the first call. Restart Claude Desktop and the four tools appear.

Related MCP server: companies-house-mcp

What each tool returns

This is copied from an actual run against the live API. The >>> and <<< layout is a tidied rendering of the MCP client's output, reformatted for readability; the request arguments and the response bodies are untouched. The requests are the lines beginning >>>, the responses follow.

>>> company_search {"query":"tesco","items_per_page":2}
<<<
{
  "query": "tesco",
  "total_results": 356,
  "items_per_page": 2,
  "start_index": 0,
  "items": [
    {
      "company_number": "00445790",
      "title": "TESCO PLC",
      "company_status": "active",
      "company_type": "plc",
      "date_of_creation": "1947-11-27",
      "address_snippet": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA"
    },
    {
      "company_number": "09384423",
      "title": "KFORD TYRES (GORNAL) LTD",
      "company_status": "active",
      "company_type": "ltd",
      "date_of_creation": "2015-01-12",
      "address_snippet": "2 Dawley Brook Road, Kingswinford, England, DY6 7BD"
    }
  ]
}

>>> company_profile {"company_number":"00445790"}
<<<
{
  "company_number": "00445790",
  "company_name": "TESCO PLC",
  "company_status": "active",
  "company_type": "plc",
  "data_note": "This profile is a fixed projection. Only the fields present here are fetched. If a property you need (for example charges or insolvency history) is not present, it was not retrieved, not necessarily false.",
  "date_of_creation": "1947-11-27",
  "jurisdiction": "england-wales",
  "registered_office_address": {
    "address_line_1": "Tesco House, Shire Park",
    "address_line_2": "Kestrel Way",
    "locality": "Welwyn Garden City",
    "postal_code": "AL7 1GA",
    "country": "United Kingdom"
  },
  "sic_codes": ["47110"],
  "accounts": {
    "next_due": "2027-08-26",
    "next_made_up_to": "2027-02-26",
    "overdue": false
  }
}

>>> company_officers {"company_number":"00445790","items_per_page":2}
<<<
{
  "company_number": "00445790",
  "active_count": 11,
  "resigned_count": 63,
  "total_results": 74,
  "items_per_page": 2,
  "start_index": 0,
  "items": [
    { "name": "TAYLOR, Christopher Jon", "officer_role": "secretary", "appointed_on": "2025-04-14" },
    {
      "name": "BETHELL, Melissa",
      "officer_role": "director",
      "appointed_on": "2018-09-24",
      "nationality": "British",
      "country_of_residence": "United Kingdom",
      "date_of_birth": { "month": 9, "year": 1974 }
    }
  ]
}

>>> company_filing_history {"company_number":"00445790","items_per_page":2}
<<<
{
  "company_number": "00445790",
  "total_count": 8372,
  "items_per_page": 2,
  "start_index": 0,
  "items": [
    {
      "transaction_id": "MzU0MzY3ODg5MGFkaXF6a2N4",
      "category": "capital",
      "date": "2026-09-16",
      "type": "SH03",
      "description": "Return of purchase of own shares",
      "description_code": "capital-return-purchase-own-shares",
      "pages": 4,
      "paper_filed": true
    },
    {
      "transaction_id": "MzUzOTMzNzQ1OWFkaXF6a2N4",
      "category": "capital",
      "date": "2026-08-26",
      "type": "SH06",
      "description": "Cancellation of shares",
      "description_code": "capital-cancellation-shares",
      "pages": 4,
      "paper_filed": true
    }
  ]
}

>>> company_profile {"company_number":"not-a-number"}
<<< [isError]
{ "ok": false, "error": { "code": "INVALID_COMPANY_NUMBER", "message": "\"not-a-number\" is not a valid company number. Expected eight characters: digits, or a two-letter prefix then six digits, for example 00000006 or SC123456." } }

>>> company_profile {"company_number":"99999999"}
<<< [isError]
{ "ok": false, "error": { "code": "COMPANY_NOT_FOUND", "message": "No company exists for that number." } }

A few things in that run are the whole point. The search carries company_status, company_type, date_of_creation and an address on every hit, so the model can pick Tesco PLC over a tyre fitter in Kingswinford without a second call. The two error cases come back as ordinary results the model can read, flagged isError, each with a code it can branch on. And the invalid number never reached the API: it was rejected locally.

The figures are whatever the register held the day I ran this: 356 companies match "tesco", Tesco PLC has 8,372 filings and 74 officer appointments on record, and its accounts are next due on 26 August 2027.

Claude choosing the tools itself

The block above proves the server responds. This proves the contract works. I wired the four tools into Claude with nothing else connected and asked a plain question. The exchange below is from a headless claude run with the server attached; I reformatted it, cut the CLI's own internal bookkeeping, and shortened the final answer, but the tool calls and the order Claude made them in are exactly as they happened.

Me:     Who runs Tesco and when are their next accounts due?

Claude: I'll search Companies House for Tesco, then pull the officers and
        profile for the right entity.
          company_search   {"query":"Tesco PLC","items_per_page":10}
          company_profile  {"company_number":"00445790"}
          company_officers {"company_number":"00445790","items_per_page":50}

Claude: Tesco PLC (00445790) is active, registered at Tesco House, Welwyn
        Garden City. Its next accounts are due 26 August 2027, made up to
        26 February 2027, not overdue. The register lists 11 active officers,
        among them Ken Murphy (director, appointed 1 October 2020) and
        Christopher Taylor (secretary, appointed 14 April 2025), and 63
        resigned.

Claude was never given the company number. It searched, read company_status and company_type off the search result to settle on 00445790 out of 356 matches, and chained into profile and officers without being told to. That is the search-carries-enough-to-decide design doing the job it exists for.

The contract, and why it looks like this

Four tools, not one tool with a mode. A single tool would have to mark every argument optional, because company_number is required for three of the four jobs and meaningless for search. Four tools give the model four tight schemas, so a call with a missing argument fails against the schema it was handed rather than three steps later at the API. I rejected the mode parameter for that reason.

Trimmed responses, not raw upstream JSON. A company profile from Companies House has more than thirty top-level fields. Most of them the model will never read, and they cost tokens on every call. I project down to a narrow set and, while I am there, fix an upstream inconsistency: the profile calls the company type type, search calls it company_type, and I expose one name. The cost is fidelity. If you want has_charges, this does not give it to you. I think that is the right trade for a model client, but it is a trade, and it creates one specific hazard.

The hazard is that the model cannot tell "absent because it is false" from "absent because I trimmed it". So the boundary is visible on purpose: each tool description lists the fields it returns, and the profile carries a data_note saying as much. I rejected an include_fields parameter, which would have let the model pull extras but would also have reopened the gap between the schema and the projection that sealing the contract is meant to close.

Errors split in two. Not-found, invalid-number, rate-limited and upstream-unavailable come back as readable results, because the model can act differently on each. A missing or bad key does not: that is an operator problem, so the server validates the key at startup and refuses to run without one. A 401 arriving mid-session, after a key that worked, is its own code so the model reports it instead of retrying.

Paging is capped at 100 per call. Companies House documents no maximum, which surprised me, so the cap is mine and the code says so. Uncapped, a model could ask for ten thousand filings and spend the whole rate limit on one question.

That rate limit, 600 requests per five minutes per key, is handled before the fact, not discovered by being refused. A sliding-window limiter holds requests under the line and waits for a slot; if the wait would be too long it returns RATE_LIMITED with a seconds-to-wait rather than blocking forever. I would rather the server know the limit than learn it from a 429.

What it does not do

It covers four endpoints. No charges, no persons-with-significant-control, no document downloads, no insolvency detail. I scoped it to the things a person actually asks about a company and stopped.

The filing description table is the weak point. Companies House returns filing descriptions as enum keys, readable only through a reference file it publishes separately. I bundled a hand-copied subset of that file, so an unseen code falls back to a prettified version of the key. It works, and the original code is always returned alongside, but a hand-copied table goes stale silently. With more time I would pull the full enumeration file at build time instead of maintaining a slice of it by hand.

Two other things I would change. The cache is per-process and in-memory with a TTL but no size bound, which is fine for a desktop tool serving one person and wrong for anything larger. And the rate limiter is per-process too, so two servers sharing one key could still exceed the limit between them; a shared limiter would fix it and is not worth the complexity here.

Tests

npm test

The suite runs against fixtures captured from real responses and never touches the network. The fixtures were taken from companies 00000006 (dissolved) and 00445790 (active) so both the cessation path and the upcoming-accounts path are covered by real data. npm run test:live adds three tests that hit the real API, and is skipped unless you set a key. npm run capture regenerates the fixtures from live responses.

Licence

MIT. See LICENSE.

Available Tools

4 tools
company_filing_historyUK company filing historyA

List the filing history of one UK company by its number. Returns items each with transaction_id, category, date, type, a resolved description, the original description_code, pages and paper_filed. Filter with the category parameter. Paged. On failure returns an error object with a code field.

NameRequiredDescriptionDefault
categoryNoOptional comma-separated categories to include, for example 'accounts,confirmation-statement'. Omit for all categories.
start_indexNoZero-based offset of the first filing. Default 0.
company_numberYesCompanies House number, for example 00000006 or SC123456.
items_per_pageNoFilings per page. Default 25, maximum 100.

TDQS

A3.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does disclose key behavior: the item fields returned, that results are paged, that category filtering is supported, and that failures return an error object with a code field. It omits result ordering, auth/rate-limit requirements, and what a missing company number yields, which keeps it below a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four compact sentences, front-loaded with purpose, then returns, filtering, paging and error shape. No filler, though the return-field enumeration is dense enough that a reader must parse it mid-sentence.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, and the description compensates by enumerating the returned item fields and the error object shape, plus paging behavior. An agent has enough to call it correctly; only auth, ordering and empty-result semantics are missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so start_index, items_per_page and company_number are fully documented in the schema. The description only restates the category filter, adding no semantics beyond the schema — the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('List the filing history of one UK company by its number'), which is clearly distinct in kind from company_search, company_profile and company_officers. It does not explicitly name the siblings, but the resource ('filing history') is a natural discriminator.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied by the resource name — an agent can infer it should call this when it needs filings rather than profile data. No explicit when/when-not guidance or named alternatives (e.g. 'use company_profile for registered-office and status details') are provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

company_officersUK company officersA

List the officers of one UK company by its number. Returns active_count and resigned_count, then items each with name, officer_role, appointed_on, resigned_on, nationality, occupation, country_of_residence and date_of_birth (month and year only). Paged. On failure returns an error object with a code field.

NameRequiredDescriptionDefault
order_byNoSort order for the officers list.
start_indexNoZero-based offset of the first officer. Default 0.
company_numberYesCompanies House number, for example 00000006 or SC123456.
items_per_pageNoOfficers per page. Default 35, maximum 100.

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does substantial work: it discloses the return structure (active_count, resigned_count, items fields), pagination, and error behavior. It stops short of stating permissions/auth requirements or rate limits, but is otherwise informative for a read/list operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and scope, then efficiently covers return fields, pagination, and error behavior. Every sentence carries useful information with no wasted preamble.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema and no annotations, the description fills the key gaps: it enumerates returned counts and item fields, notes pagination, and describes the error object. This is sufficient for an agent to call and interpret the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the schema already documents company_number, order_by, start_index, and items_per_page. The description adds only 'by its number' and 'Paged,' which does not add meaningful semantics beyond the schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: 'List the officers of one UK company by its number.' It is clearly distinct from company_search, company_profile, and company_filing_history by resource, but does not explicitly name those siblings or contrast itself with them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by 'by its number,' making clear this requires a known company number rather than a search query. However, there is no explicit when-to-use or when-not-to-use guidance relative to the sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

company_profileUK company profileA

Get the profile of one UK company by its number. Returns company_name, company_number, company_status, company_type, date_of_creation, date_of_cessation, jurisdiction, registered_office_address, sic_codes and accounts due dates. This is a fixed subset; a property not listed was not fetched, so do not read its absence as false. On failure returns an error object with a code field, including COMPANY_NOT_FOUND and INVALID_COMPANY_NUMBER.

NameRequiredDescriptionDefault
company_numberYesCompanies House number, for example 00000006 or SC123456. Eight characters, digits or letters. Case is normalised.

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It usefully discloses the returned fields, a fixed-subset caveat ('a property not listed was not fetched, so do not read its absence as false'), and specific error codes. It does not cover auth or rate limits, but for a simple read tool the behavioral disclosure is strong.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, front-loaded with purpose, then output fields, then error behavior. Every sentence earns its place, and the fixed-subset warning is a valuable clarification rather than filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema and no annotations, the description compensates by listing the returned fields, explaining the fixed-subset semantics, and documenting error codes. This is complete enough for an agent to call the tool correctly and interpret the response.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage for the single company_number parameter, including format examples and case-normalisation behavior. The tool description adds no additional semantics beyond 'by its number', so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: 'Get the profile of one UK company by its number.' The scope ('one', 'by its number') implicitly distinguishes it from a search or list tool, but it does not explicitly name or contrast with sibling tools like company_search.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by 'by its number' – the agent should already have a company number. However, there is no explicit guidance on when to use this versus company_search, nor any exclusions or prerequisites. The minimum viable signal is present but alternatives are left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 4 tool updatesv0.1.0
    • First observedcompany_filing_history
    • First observedcompany_officers
    • First observedcompany_profile
    • First observedcompany_search

TDQS

A4.1/5.0

Scored across 4 tools

Disambiguation5/5

Each tool targets a distinct operation: name search, single-company profile, officer listing, and filing history. The search tool explicitly tells the agent to use the returned company_number with the other three, and the resource/action boundaries are unambiguous.

Naming Consistency5/5

All four tools follow a consistent snake_case pattern with a shared 'company_' prefix and a clear resource noun (search, profile, officers, filing_history). No style mixing or vague verbs.

Tool Count5/5

Four tools is a tight, well-scoped surface for a read-only Companies House lookup server. Each tool covers a core operation and there are no redundant or filler tools.

Completeness3/5

The surface covers the most common lookups (search, profile, officers, filings) but omits notable Companies House resources such as charges, persons with significant control, and insolvency data. Agents needing those will hit dead ends, though core workflows are supported.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables looking up UK companies, officers, ownership, filings, and running due diligence checks via the Companies House API, usable from AI tools like Claude or Cursor.
    3 npm
    13
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables AI assistants to search and retrieve UK Companies House data including company profiles, officers, and filing history via the official API.
    4
    167 npm
    1
    MIT
  • F
    license
    B
    quality
    D
    maintenance
    Provides access to UK Companies House public data, enabling search and retrieval of company profiles, officers, filing history, and more through natural language queries.
    12
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables querying UK company data including search, profiles, officers, filings, and persons with significant control via Companies House API.
    167 npm
    MIT