companies-house-mcp
Read-only access to the UK Companies House register through seven tools for company due diligence and ownership questions.
Search companies by name to get company numbers, status and basic details (live or dissolved).
Get a company profile: status, type, incorporation date, registered office, SIC codes, previous names, accounts/confirmation statement deadlines, and derived warnings for overdue filings, insolvency, strike-off, or address problems.
List officers (directors, secretaries, LLP members) with appointment dates, occupation, nationality, birth month/year, and optional resigned officers.
List filings (accounts, confirmation statements, officer changes, charges, resolutions, etc.) by category with form codes, readable descriptions, and document availability.
List charges/secured lending: lenders, creation/delivery/satisfaction dates, fixed/floating charges, status, and optional outstanding-only filtering.
Get registered office history: current address and recorded changes, including moves to Companies House default and old/new addresses where available.
List persons with significant control (PSCs): owners/controllers, natures of control, corporate owners with registration numbers for ownership-chain follow-up, and optional ceased entries.
All tools are read-only, idempotent, support paging, and return model-friendly output; they cannot search officers, download documents, or modify the register.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@companies-house-mcp Is Acme Ltd still trading?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
companies-house-mcp
An MCP server that gives a model read-only access to the UK Companies House register: every company in England, Wales, Scotland and Northern Ireland, live or dissolved. With it, a model can answer questions like "is this supplier still trading?", "who actually owns this company?" and "has anyone lent money against it?" from the register itself, not from memory.
Seven tools, all reads. Python, the official MCP SDK (mcp 2.x), httpx, pydantic.
Tested offline without an API key, and checked against the live register.
search_companies find a company by name, get its number
get_company_profile status, type, office, activities, deadlines, red flags
list_officers directors, secretaries, LLP members
list_filings what it has filed, in words
list_charges secured lending against it
get_registered_office_history where it has been registered, and when it moved
list_persons_with_significant_control who owns or controls itMost of this README is about why the server looks the way it does. The decisions matter more than the code: wrapping an API takes an afternoon; choosing what a model should see is the actual work.
Design decisions
Seven tools, not thirty
The Companies House API has around thirty read endpoints. A server that mirrors them one to one is easy to write and worse to use. Every tool's name, description and schema sit in the model's context for the whole conversation, whether it is used or not. And every extra tool is one more wrong choice on offer: tools that overlap ("search" and "advanced search", "profile" and "registered office") are where models pick badly.
So the tools follow the questions people ask about a company, not the endpoints:
Question | Tool | Upstream requests |
Which company do you mean? |
| 1 |
What is it, and is it healthy? |
| 1 |
Who runs it? |
| 1 (2 if the list is empty) |
What has it filed? |
| 1 (2 if the list is empty) |
Who has security over it? |
| 1 (2 if the list is empty) |
Where has it been based? |
| 2 |
Who owns it? |
| 1 (2 if the list is empty) |
One tool, get_registered_office_history, has no endpoint of its own. Companies House
serves the current address and, separately, the filings that changed it. The model
wants the history, so the tool joins the two.
The whole tool surface (names, descriptions, input schemas) is under 8,000 characters,
roughly 2,000 tokens. tests/test_surface.py fails the build if it passes 12,000, so
any growth is a decision rather than drift.
Every tool is a read, and says so
The public data API is read-only anyway; filing uses a separate API with per-company
authentication codes. But the server also tells the host so: each tool carries
readOnlyHint, idempotentHint and openWorldHint, and destructiveHint: false.
That matters in practice. A host can let the model run these tools without asking the
user each time, which a tool that could change the register would never earn. Mixing
reads and writes in one server makes the host treat all of it as the riskier kind.
What is deliberately not exposed
Officer search and officer appointment history.
/search/officersand/officers/{id}/appointmentsturn a company tool into a person tool: "every company this person has ever been involved in". Due diligence has real uses for that, but it is a different risk profile, and it belongs in a separately reviewed server a host opts into, not bundled here.Home and service addresses of officers and PSCs. The API returns them. The models here do not. No company question needs a director's correspondence address.
Advanced search (by activity code, incorporation date, location). It is useful for building lists of companies, but lists are not what this server is for, and it is the natural starting point for bulk harvesting.
Document downloads. Accounts and forms are PDFs, often scans. Fetching them is a different job (size, OCR, caching) and would sit badly inside a quick lookup.
list_filingssays whether a document exists.Lower-frequency resources: insolvency case detail, disqualified officers, exemptions, UK establishments, statutory registers, PSC statements. Each is one small tool away when a real question needs it. The gap most likely to matter is PSC statements, so
list_persons_with_significant_controlsays so: an empty list may mean the company filed a statement instead.The streaming API. It is push, not pull, and does not fit the request and response shape of a tool call.
Results shaped for a model, not for a web page
The raw API is built for the Companies House website: nested address objects,
enumeration keys where words should be, links, etags, and many fields no answer needs.
A model pays for each of those tokens and then has to work out what they mean. So
every tool returns its own pydantic model, published as an MCP output schema, with:
Words, not codes.
"ltd"becomes "Private limited company"; a filing's"appoint-person-director-company-with-name-date"plus its values becomes "Appointment of Mr Tomasz Brandt as a director on 2019-11-04". The text comes from Companies House's own lookup tables (api-enumerations), vendored as JSON byscripts/sync_enumerations.py, so it matches their website exactly. An unknown key degrades to readable words instead of disappearing.Red flags derived, not left to inference.
get_company_profilereturns awarningslist: overdue accounts, an overdue confirmation statement, liquidation, proposal to strike off, insolvency history, a disputed or undeliverable registered office. Without it, a model has to notice that one boolean among twenty is true. Its schema also says what an empty list means: none found, not "vetted".Defaults that answer the usual question. "Who are the directors?" means current ones, so resigned officers and ceased PSCs are left out unless asked for. The result still carries the whole-company counts and how many rows were dropped, so the model knows what it isn't seeing.
Paging the model can't get wrong. Every list returns
next_start_index, which is null at the end and never points past an empty page, so there is no loop to get stuck in.Chains the model can follow. A corporate owner comes back with its registration number, and the description says to pass it to
get_company_profile. That is how "who ultimately owns this?" gets answered, one hop at a time.Limits in the schema. Page sizes are capped at 100 and filing categories are an enum, so a bad call fails validation before it costs a request against the key's quota.
Example: the profile of a company in trouble (from the test fixtures):
{
"company_number": "SC654321",
"name": "NORTHGATE PLANT HIRE LTD",
"status": "Liquidation",
"registered_office": "c/o Firth & Mowat Recovery LLP, 3 Castle Wynd, Inverness, IV2 3EQ, Scotland",
"warnings": [
"Company status is Liquidation.",
"Status detail: Active proposal to strike off.",
"Accounts are overdue, due by 2025-12-02.",
"Confirmation statement is overdue, due by 2025-03-15.",
"The company has an insolvency history on the register.",
"The registered office address is in dispute.",
"Mail to the registered office has been returned as undeliverable."
]
}Inputs forgiving where it is safe, strict where it isn't
Models write company numbers the way people do: 445790, sc 123456,
Company No. 04 12 34 56. All of those normalise to the canonical eight characters.
What cannot be a company number (TESCO, nine digits) is refused before any request,
with a message saying what a valid number looks like and to call search_companies if
all you have is a name. Sending it upstream would come back as a 404, and a model
reads a 404 as "this company does not exist". That is a different and wrong answer.
Errors written for the model that has to handle them
Every expected failure reaches the model as a message that says what happened and what to do next:
Situation | What the model is told |
Malformed number | what a valid one looks like; use |
No such company | the number isn't on the register; search by name |
Rate limited | wait about N seconds; don't retry straight away |
API down or timing out | the fault is upstream; try again in a minute, don't guess |
No key / bad key | server misconfigured; retrying won't help |
Unexpected exceptions are not dressed up: the SDK reports them as a generic tool failure and nothing internal leaks into the conversation.
One case needs special handling, and the live API is what showed it (see below). Ask
Companies House for the charges, officers, filings or PSCs of a company that does not
exist and it answers 200 with an empty list. To a model that reads as "this company has
no charges", which is the wrong answer to a question about a company that isn't there.
So an empty first page is checked against the company profile, which does 404 for a
missing company: if the company exists the empty list stands; if not, the model gets
CompanyNotFound. The extra request is spent only when the answer is empty.
Rate limiting that neither hangs nor hammers
The API allows 600 requests per five minutes per key, and a model fanning out across
twenty search results can hit that in seconds. The limiter keeps a rolling window of
its own requests. A short wait (up to 5 seconds) is absorbed quietly. A long wait is
refused at once with the real number of seconds, because a tool call that hangs for four
minutes is worse for an agent than an error it can plan around. The limiter also reads
the server's X-Ratelimit-Remain / X-Ratelimit-Reset headers, so it stays honest
when another process shares the key. Transient failures (502, 503, 504, connection
errors, timeouts) get exactly one retry; a 500 does not, because a request that broke
the server once will break it again.
Built against the spec, then run against the real thing
The first version was written and tested against the published specification only, before an API key existed. Then it was run against the live register: five companies recorded (Tesco PLC, Deloitte LLP, Woolworths Limited, the old Woolworths company now in liquidation, and a dissolved Scottish company), 35 responses in all. Here is what the live API contradicted, and what it confirmed.
Assumption from the spec | What the live API does | Consequence, and the fix |
A company that doesn't exist gets a 404 | Only on the profile. Its charges, officers, filings and PSCs come back 200 with an empty list | The server would have told a model a non-existent company "has no charges". Empty lists are now confirmed against the profile |
The rate-limit header is | It is | The limiter never saw the server's count. It reads the real header now, and the test fixtures use it |
Filings in the | The category also holds register inspection address changes (AD02), register moves (AD03) and register-of-members locations (353) | Tesco's history showed inspection-address changes as office moves. Office moves are now matched by description key, from the register's own tables |
Office changes are AD01 forms | Companies House can move an office itself, to its default address in Cardiff (RP05), when the real one is shown to be wrong | That is a red flag, so it is flagged in the history, and the profile warns when the current office is the default address |
Address values are clean text | They arrive as | Tidied before a model sees them |
Old changes have old and new addresses | Paper-era forms (287, LLP287) carry only free text | Kept, with the text in |
Confirmed as assumed: charge particulars, classification and secured_details
arrive as objects, not the arrays the spec describes (the shaping already accepted
both); every filing description key across the five histories was found in the
vendored tables; an empty search is 200 with total_results: 0.
Last, a live run through a real host. Claude Code, with only this server connected,
was asked who owns the active Woolworths Limited and whether company 00000001 has any
charges. It called search_companies, then list_persons_with_significant_control and
list_charges in parallel, then get_company_profile. It answered that Woolworths
Limited is controlled by Littlewoods Limited (active, 00262152) and that 00000001 is not
on the register. Before the fix above, it would have said 00000001 had no charges.
Related MCP server: companies-house-mcp
Using it
You need Python 3.11+ and a free API key from the Companies House developer hub (create an application, then a REST key).
git clone <this repo> && cd companies-house-mcp
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .Claude Code
claude mcp add companies-house -e COMPANIES_HOUSE_API_KEY=your-key -- companies-house-mcpClaude Desktop (or any host that takes this config shape)
{
"mcpServers": {
"companies-house": {
"command": "/path/to/.venv/bin/companies-house-mcp",
"env": { "COMPANIES_HOUSE_API_KEY": "your-key" }
}
}
}It runs over stdio by default. companies-house-mcp --transport streamable-http --port 8000
serves it over HTTP instead. Without a key the server still starts and lists its tools,
and every call explains that the key is missing.
Variable | Default | |
| none | required for calls |
| the live API | e.g. a sandbox |
|
| seconds per request |
Tests
pip install -e ".[dev]"
pytest # 127 tests, no network, no key
ruff check . && ruff format --check .
mypy # --strict over src/, per pyproject.toml127 tests, all offline. GitHub Actions runs exactly those three commands on Python 3.11
and 3.12 for every push and pull request; the badge above is that workflow. mypy is
configured strict with files = ["src"], so the strict guarantee covers the package,
not the tests and scripts.
The suite fakes Companies House at the HTTP layer (httpx.MockTransport), so the real
client, limiter, retry and error mapping run in every test. Tool tests talk to the
server through the SDK's in-process MCP client, the same path a host takes, and one test
starts the server as a subprocess over stdio. That one catches packaging mistakes and
stray output that would corrupt the protocol stream. test_surface.py checks the
contract itself: exactly these seven tools, all annotated read-only, every input
documented, page sizes capped, and the context budget.
There are two kinds of fixture. The synthetic ones (invented companies, shaped on the
published spec, with the live API's behaviour folded back in) drive the behaviour
tests. The recorded ones in tests/fixtures/recorded/ are real responses from the
live register, made by scripts/record_fixtures.py, and tests/test_recorded.py runs
the shaping layer over every one, so drift in the live API shows up as a failing test.
Before a recording is written, natural persons are redacted: individual officers' and
PSCs' names, birth dates, addresses and officer ids, and officer names inside filing
text. The server never returns officers' addresses, and its fixtures don't publish
them either. Companies and corporate officers are kept as recorded.
Layout
src/companies_house_mcp/
server.py tools, descriptions, argument schemas, entry point
client.py HTTP: auth, retries, status codes to errors, the empty-list check
ratelimit.py sliding-window limiter that also obeys the server's headers
models.py what the tools return (the output schemas)
shaping.py raw API JSON to those models; total, never throws on odd records
company_number.py normalisation and validation
errors.py every expected failure, with its model-facing message
enumerations.py Companies House lookup tables (data/enumerations.json)
scripts/ refresh the lookup tables; record live fixtures
tests/ fake API, tool tests over MCP, stdio test, surface contractNotes
The register holds what companies filed. It is authoritative for what was filed and when, not proof that the contents are true. The server's instructions tell the model as much.
Register data comes from Companies House and is subject to their terms. The vendored lookup tables come from
companieshouse/api-enumerations.
Licence
MIT. See LICENSE.
Available Tools
7 toolsget_company_profileARead-onlyIdempotent
Get a company's register entry: name, status, type, incorporation date, registered office, business activities (SIC codes), previous names, and filing deadlines.
Read warnings first: it lists overdue accounts or confirmation statements,
insolvency history, proposals to strike off, and a disputed or undeliverable
registered office. has_charges says whether list_charges will find anything.
One upstream request.
Inputs: company_number (required). Returns: name, type, status, accounts, +12 more
get_registered_office_historyARead-onlyIdempotent
Get a company's current registered office and every recorded change to it, newest first, with old and new addresses where the register has them.
Frequent moves, or a move shortly before insolvency, are worth pointing out. Two upstream requests.
Inputs: company_number (required). Returns: changes, complete, company_number, current_address
list_chargesARead-onlyIdempotent
List charges (secured lending, such as mortgages and debentures) registered against a company: who holds them, when created, whether satisfied, and whether a floating charge covers everything the company owns.
A company with no charges returns an empty list, not an error.
Inputs: company_number (required), limit, start_index, outstanding_only. Returns: total, charges, satisfied, outstanding, +2 more
list_filingsARead-onlyIdempotent
List what a company has filed at Companies House, newest first: accounts, confirmation statements, officer changes, charges, resolutions and so on, each with its form code and a readable description.
Use category rather than paging through everything. Document contents are not
returned, only whether a copy exists.
Inputs: company_number (required), limit, category, start_index. Returns: total, filings, category, company_number, +1 more
list_officersARead-onlyIdempotent
List a company's directors, secretaries and LLP members, with appointment dates, occupation, nationality and month and year of birth.
active_count and resigned_count cover the whole company even when one page does
not. Addresses are deliberately left out.
Inputs: company_number (required), limit, start_index, include_resigned. Returns: officers, active_count, company_number, resigned_count, +2 more
list_persons_with_significant_controlARead-onlyIdempotent
List who owns or controls a company (its persons with significant control): the people and companies holding over 25% of shares or votes, or the right to appoint the board, and how.
A corporate owner comes with its registration number; if it is a UK company, pass that to get_company_profile to follow the chain upwards. An empty list can mean the company filed a statement that it has no PSC, which this tool does not read.
Inputs: company_number (required), limit, start_index, include_ceased. Returns: people, active_count, ceased_count, ceased_omitted, +2 more
search_companiesARead-onlyIdempotent
Find companies on the UK register by name, returning their company numbers.
Covers live and dissolved companies alike, ranked by relevance; check status
before assuming a hit is trading. Use the returned company_number with every
other tool. It searches company names only, so it cannot find a company from a
director's name.
Inputs: query (required), limit, start_index. Returns: query, results, total_results, next_start_index
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 7 tools
Each tool addresses a distinct Companies House resource: companies, profiles, filings, officers, charges, registered office history, and PSC. There is no meaningful overlap or ambiguity between them.
Tool names follow a clear verb_noun pattern: list_* for collections and get_* for singular resources, with search_companies as the only search exception, which is still a common and predictable convention. Naming is uniform in style and case.
Seven tools is well-scoped for the Companies House domain, covering the major public register endpoints without unnecessary bloat or fragmentation. Each tool corresponds to a meaningful API operation and earns its place.
The core Companies House surface is covered: search, company profile, filings, officers, charges, registered office history, and PSC. Minor gaps remain, such as not providing filing document contents or PSC statement details, but these do not significantly impair typical workflows.
Related MCP Connectors
UK Companies House company data (company-level, change tracking) for AI agents.
CompanyLens is a remote MCP server giving AI agents instant access to official company registry data across 19 jurisdictions in Europe, the Americas, and Asia-Pacific. Eighteen read-only tools let you search companies and people, look up officers and beneficial owners, map corporate networks through shared directors, screen names against the UK disqualified directors register, find every company at a registered address, and pull filing history — all from a single connector. Visit our website: https://companylens.io
Company data for AI agents: enrich a domain, UK Companies House records, Y Combinator companies.
UK company records from Companies House, with alerts on new filings, officer and status changes.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI assistants to search and retrieve UK Companies House data including company profiles, officers, and filing history via the official API.4196 npm1MIT
- FlicenseBqualityDmaintenanceProvides access to UK Companies House public data, enabling search and retrieval of company profiles, officers, filing history, and more through natural language queries.12-
- AlicenseNot gradedqualityDmaintenanceEnables querying UK company data including search, profiles, officers, filings, and persons with significant control via Companies House API.196 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables querying UK Companies House data via natural language, including company profiles, officers, filing history, and other statutory records.308 npmMIT