Skip to main content
Glama
EdwardRadford

companies-house-mcp

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 it

Most 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?

search_companies

1

What is it, and is it healthy?

get_company_profile

1

Who runs it?

list_officers

1 (2 if the list is empty)

What has it filed?

list_filings

1 (2 if the list is empty)

Who has security over it?

list_charges

1 (2 if the list is empty)

Where has it been based?

get_registered_office_history

2

Who owns it?

list_persons_with_significant_control

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/officers and /officers/{id}/appointments turn 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_filings says 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_control says 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 by scripts/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_profile returns a warnings list: 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 search_companies

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 X-Ratelimit-Remaining

It is X-Ratelimit-Remain (with -Limit: 600, -Window: 5m, -Reset)

The limiter never saw the server's count. It reads the real header now, and the test fixtures use it

Filings in the address category are registered-office moves

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 , Tesco House, Delamare Road,, Cheshunt,, Herts

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 description and the address fields left null

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: companieswise

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-mcp

Claude 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

COMPANIES_HOUSE_API_KEY

none

required for calls

COMPANIES_HOUSE_BASE_URL

the live API

e.g. a sandbox

COMPANIES_HOUSE_TIMEOUT

10

seconds per request

Tests

pip install -e ".[dev]"
pytest            # no network, no key
ruff check . && mypy

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 contract

Notes

  • 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 tools
get_company_profileA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
company_numberYesCompanies House company number, e.g. 00445790 or SC123456. Spaces and missing leading zeros are fine. Get it from search_companies if you only have a name.

Output Schema

ParametersJSON Schema
NameRequiredDescription
nameYes
typeYes
statusYes
accountsNo
warningsNoPlain-English red flags derived from the record: overdue filings, insolvency, a disputed, undeliverable or Companies House default registered office. Empty means none were found, not that the company has been vetted.
sic_codesNoDeclared business activities.
has_chargesNoTrue if any charge (secured lending) was ever registered.
dissolved_onNo
jurisdictionNo
status_detailNo
company_numberYes
previous_namesNo
incorporated_onNo
registered_officeNoOne line, as on the register.
confirmation_statementNo
has_insolvency_historyNo

TDQS

A4.3/5.0
Behavior4/5

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

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is covered structurally. The description adds genuine value beyond this: it flags that `warnings` must be read first and enumerates what it lists (overdue accounts, insolvency, strike-off proposals, disputed/undeliverable registered office), and discloses 'One upstream request' as a performance characteristic. This contextualizes behavior without contradicting the annotations.

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 compact, purpose-built sentences: the first defines scope, the second delivers behavioral guidance (read warnings first, has_charges semantics), and the third is a one-line performance note. No filler, no repetition of annotation data, and the most important scoping information is front-loaded.

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?

The presence of an output schema relieves the description of explaining return values. For a single-parameter, read-only, idempotent tool with full annotation coverage, the description covers the key operational facts: what fields come back, which field to inspect first, the flag that gates a sibling call, and the upstream cost. Nothing an agent needs to invoke it correctly is 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% and the company_number parameter carries a thorough schema description (format, example, tolerance for spaces/missing zeros, and routing to search_companies). With the schema doing the heavy lifting, the baseline of 3 applies. The tool description itself adds no parameter detail, but none is needed given the schema's completeness.

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

Purpose5/5

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

The description opens with a specific verb+resource pair ('Get a company's register entry') and enumerates the exact fields returned (name, status, type, incorporation date, SIC codes, previous names, filing deadlines). This specificity lets an agent distinguish it from siblings like list_filings, list_officers, and search_companies without opening any schemas.

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

Usage Guidelines4/5

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

The description references the sibling relationship: `has_charges` tells whether list_charges will find anything, implying when to chain to that tool. Additionally, the parameter description (in schema) routes agents to search_companies when only a name is known. This provides clear context, though it does not explicitly frame when-not-to-use scenarios or name exclusions.

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

get_registered_office_historyA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
company_numberYesCompanies House company number, e.g. 00445790 or SC123456. Spaces and missing leading zeros are fine. Get it from search_companies if you only have a name.

Output Schema

ParametersJSON Schema
NameRequiredDescription
changesYesNewest first.
completeYesFalse if there were more address filings than one page holds. Paper-era changes can also lack the old and new address text.
company_numberYes
current_addressYes

TDQS

A4.3/5.0
Behavior4/5

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

Annotations already establish readOnlyHint, idempotentHint, and destructiveHint=false. The description adds useful behavioral context beyond that: results are ordered newest first, addresses are included only 'where the register has them', and there are two upstream requests. It stops short of describing error/rate-limit behavior, but the annotations lower the bar.

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 short sentences, all substantive: the first defines the exact scope and ordering, the second gives an analytical trigger, and the third states request cost. There is no filler, and the core purpose is front-loaded.

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 one required parameter, a rich schema description, an output schema, and annotations covering read-only/idempotent/non-destructive behavior, the description supplies everything needed to call the tool correctly: purpose, result ordering, data completeness caveat, and upstream cost.

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%, and the schema description already explains company_number with format examples, tolerated input variations, and a fallback to search_companies. The tool description itself does not add parameter-level meaning, so the baseline of 3 is appropriate.

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

Purpose5/5

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

The description uses a specific verb and resource: '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.' It clearly defines scope, ordering, and what data is included, and it is easily distinguished from siblings like get_company_profile by the historical dimension.

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

Usage Guidelines4/5

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

The description gives clear context on when the result is analytically important ('Frequent moves, or a move shortly before insolvency, are worth pointing out') and warns about cost ('Two upstream requests'). It does not explicitly name alternatives or exclusions, but no sibling offers this same history scope, so the guidance is sufficient.

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

list_chargesA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoCharges per page.
start_indexNoOffset for paging. Use next_start_index from a previous result.
company_numberYesCompanies House company number, e.g. 00445790 or SC123456. Spaces and missing leading zeros are fine. Get it from search_companies if you only have a name.
outstanding_onlyNoDrop fully satisfied charges. The counts still cover all charges.

Output Schema

ParametersJSON Schema
NameRequiredDescription
totalYes
chargesYes
satisfiedYes
outstandingYesOutstanding or only part-satisfied.
company_numberYes
next_start_indexNoPass this as start_index to get the next page. Null when there are no more results.

TDQS

A4.3/5.0
Behavior4/5

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

Annotations already declare read-only, idempotent, non-destructive behavior. The description adds useful beyond-annotation context: that a company with no charges returns an empty list rather than an error, and what kinds of charge information are included. This is meaningful behavioral disclosure without contradicting the annotations.

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?

The description is compact and front-loaded: the first sentence defines the resource and returned information, and the second sentence preempts an important edge case. There is no repetition of schema details or annotation fields, and every sentence earns its place.

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?

The combination of a rich input schema, an output schema, strong annotations, and the description's edge-case note makes this complete for an agent to invoke correctly. The description covers the key behavioral exception, while the schema and annotations handle parameters, return shape, and safety profile.

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 description coverage is 100%, and the parameter descriptions are detailed, including company_number formatting, pagination semantics, and outstanding_only behavior. The tool description itself adds little parameter-specific meaning, so the baseline of 3 is appropriate.

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

Purpose5/5

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

The description uses a specific verb and resource: 'List charges registered against a company' and enumerates the key returned aspects (holder, creation, satisfaction, floating charge scope). This clearly distinguishes it from sibling tools focused on filings, officers, PSCs, and company profile data.

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

Usage Guidelines4/5

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

The description clearly positions this tool as the way to get charge information for a company and even clarifies edge-case behavior with an empty list. It does not explicitly name alternatives or exclusion conditions, but the resource categories of sibling tools are distinct enough that the usage context is clear.

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

list_filingsA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoFilings per page.
categoryNoOnly filings of this kind, e.g. 'accounts' or 'officers'. Omit for everything.
start_indexNoOffset for paging. Use next_start_index from a previous result.
company_numberYesCompanies House company number, e.g. 00445790 or SC123456. Spaces and missing leading zeros are fine. Get it from search_companies if you only have a name.

Output Schema

ParametersJSON Schema
NameRequiredDescription
totalYes
filingsYesNewest first.
categoryYes
company_numberYes
next_start_indexNoPass this as start_index to get the next page. Null when there are no more results.

TDQS

A4/5.0
Behavior4/5

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

Annotations already declare read-only, idempotent, and non-destructive behavior. The description adds observable behavioral detail beyond that: results are 'newest first,' each item includes a form code and readable description, and document contents are deliberately excluded. There is no contradiction with the annotations.

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?

The description is compact and front-loaded: the first sentence states the core purpose and output, the second gives a filtering tip, and the third sets a scope boundary. Every sentence earns its place with no redundancy.

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 read-only annotations, full parameter documentation in the schema, and the presence of an output schema, the description covers the remaining essential behavior: ordering, result contents, filtering advice, and the non-return of document contents. Nothing needed to invoke the tool correctly is missing.

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

Parameters4/5

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

Schema coverage is 100%, so the baseline is 3. The description adds a non-obvious parameter insight—'Use `category` rather than paging through everything'—which clarifies the relationship between category filtering and pagination beyond what the schema says.

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 names a specific resource ('what a company has filed at Companies House') and a clear verb ('List'), and enumerates expected filing categories like accounts, confirmation statements, and charges. It is clear on its face, though it does not explicitly contrast itself with sibling tools such as list_officers or list_charges.

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?

The description gives useful in-tool guidance: 'Use `category` rather than paging through everything.' It also warns that document contents are not returned, setting an expectation about what not to use this for. However, it never names sibling alternatives or states when to prefer this tool over list_officers, list_charges, or related tools.

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

list_officersA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoOfficers per page, before resigned ones are removed.
start_indexNoOffset for paging. Use next_start_index from a previous result.
company_numberYesCompanies House company number, e.g. 00445790 or SC123456. Spaces and missing leading zeros are fine. Get it from search_companies if you only have a name.
include_resignedNoInclude former officers. Off by default: most questions are about who is in post.

Output Schema

ParametersJSON Schema
NameRequiredDescription
officersYes
active_countYes
company_numberYes
resigned_countYes
next_start_indexNoPass this as start_index to get the next page. Null when there are no more results.
resigned_omittedNoResigned officers on this page left out because include_resigned was false.

TDQS

A4/5.0
Behavior4/5

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

Annotations already declare readOnlyHint, idempotentHint, and destructiveHint=false, so the safety profile is covered. The description adds valuable behavioral context: it warns that active_count and resigned_count span the entire company even when pagination truncates the list, and it notes that addresses are deliberately omitted. These nuances help an agent interpret results correctly and avoid assuming data completeness.

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?

The description is two short, information-dense sentences. The first sentence states the tool's purpose and output fields, and the second covers two important behavioral caveats. There is no fluff, and the key information is front-loaded.

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?

For a paginated list tool with an output schema, the description covers the essential purpose and two critical behavioral notes (company-wide counts and address omission). It does not explain pagination mechanics, but the schema documents start_index and limit. Given the output schema exists, the description is sufficiently complete for an agent to call 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?

The input schema provides a 100% description coverage for all four parameters, including defaults, ranges, and usage notes. The description itself does not elaborate on parameters beyond what the schema already states. Since the schema carries the full semantic load, a baseline score of 3 is appropriate.

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

Purpose5/5

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

The description opens with a specific verb ('List') and resource ('a company's directors, secretaries and LLP members'), then enumerates the exact data returned. This clearly differentiates it from sibling tools like list_filings or list_charges, which serve different resources.

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?

The description does not explicitly state when to use this tool versus alternatives, nor does it list exclusions. It does provide a practical hint inside the company_number parameter description—'Get it from search_companies if you only have a name'—but this is about obtaining an input, not about selecting the tool itself. The purpose is clear enough for an agent to infer appropriate usage, but explicit guidance is absent.

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

list_persons_with_significant_controlA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoEntries per page.
start_indexNoOffset for paging. Use next_start_index from a previous result.
company_numberYesCompanies House company number, e.g. 00445790 or SC123456. Spaces and missing leading zeros are fine. Get it from search_companies if you only have a name.
include_ceasedNoInclude people or entities whose control has ended.

Output Schema

ParametersJSON Schema
NameRequiredDescription
peopleYes
active_countYes
ceased_countYes
ceased_omittedNoCeased entries on this page left out because include_ceased was false.
company_numberYes
next_start_indexNoPass this as start_index to get the next page. Null when there are no more results.

TDQS

A4.3/5.0
Behavior4/5

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

Annotations already declare readOnlyHint=true, idempotentHint=true, and openWorldHint=true, so the safety profile is covered. The description adds important behavioral context beyond the annotations: an empty list can mean the company filed a no-PSC statement, which this tool does not read, and corporate owners are returned with a registration number. This prevents a common misinterpretation of an empty result.

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?

The description is front-loaded with the core definition and then delivers two high-value behavioral facts: the upward-chain follow-up via get_company_profile and the empty-list caveat. Every sentence earns its place, with no repetitive or filler content.

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 the rich schema, read-only annotations, and an output schema, the description covers everything an agent needs to invoke it correctly: what it returns, an important empty-result interpretation, and how to continue a corporate-ownership investigation. No critical operational detail is 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 description coverage is 100%, so the input schema already documents all four parameters clearly, including defaults, bounds, and the 'next_start_index' paging hint. The description adds no extra parameter-level meaning beyond what the schema provides, so the baseline of 3 is appropriate.

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

Purpose5/5

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

The description opens with the specific verb 'List' and the precise resource 'who owns or controls a company (its persons with significant control)', then defines the scope with concrete thresholds (over 25% of shares or votes, right to appoint the board). This clearly distinguishes it from sibling tools like list_officers and list_filings by focusing on ownership and control rather than directors, filings, or charges.

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

Usage Guidelines4/5

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

The description gives a clear context for use: when the agent needs to know who owns or controls a company. It also provides a concrete routing hint, telling the agent to pass a UK corporate owner's registration number to get_company_profile to trace ownership upward. It does not explicitly state when not to use this tool versus list_officers, but the ownership-focused definition makes the intended use clear.

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

search_companiesA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoResults per page.
queryYesCompany name or part of it. Not a person's name.
start_indexNoOffset for paging. Use next_start_index from a previous result.

Output Schema

ParametersJSON Schema
NameRequiredDescription
queryYes
resultsYes
total_resultsYesMatches on the whole register, not just this page.
next_start_indexNoPass this as start_index to get the next page. Null when there are no more results.

TDQS

A4.4/5.0
Behavior4/5

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

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is covered. The description adds useful behavioral context: it covers live and dissolved companies, ranks by relevance, and warns to check `status` before assuming a hit is trading. This goes beyond the annotations.

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?

The description is three sentences, each earning its place: what it does, what to watch out for, and how to use the result. It is front-loaded with the core purpose and scannable.

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?

The description is complete for a read-only search tool. It covers scope, limitations, and next steps. The output schema exists, so return values are documented. The only minor gap is that it doesn't mention pagination behavior explicitly, but the schema's `start_index` and `next_start_index` hint covers that.

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 description coverage is 100%, so the schema already documents all three parameters. The description adds context that `query` is a company name, not a person's name, which reinforces the schema's description. However, it doesn't add much beyond that, so a baseline 3 is appropriate.

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

Purpose5/5

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

The description states a specific verb ('Find'), a resource ('companies on the UK register'), and the key output ('company numbers'). It also distinguishes itself from siblings by noting it searches only company names, not directors' names, which is a clear differentiator.

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

Usage Guidelines5/5

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

The description explicitly says when to use this tool (to find companies by name) and when not to (cannot find a company from a director's name). It also instructs to use the returned company_number with every other tool, which is valuable routing guidance.

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. 7 tool updatesv0.1.0
    • First observedget_company_profile
    • First observedget_registered_office_history
    • First observedlist_charges
    • First observedlist_filings
    • First observedlist_officers
    • First observedlist_persons_with_significant_control
    • First observedsearch_companies

TDQS

A4.3/5.0

Scored across 7 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    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.
    5 npm
    13
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Provides verified UK company lookup and number validation for AI agents using official Companies House data. Enables lookup of registered details by number, validation of company number format, and search by company name.
    3
    41 npm
    Apache 2.0
  • 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
    55 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
    -