Skip to main content
Glama
kaylum54

companies-house-screening-mcp

by kaylum54

companies-house-screening-mcp

Screen UK companies against the Companies House public register from an MCP host. Batch screening of a supplier list, one-call company snapshots, and factual signals rather than a risk score.

npm CI licence: MIT

Published with npm provenance and an attested container image, so the artefact is traceable to the commit that produced it.

There is another one, and you should know about it

companies-house-mcp by @aicayzer has existed since July 2025, is at v4.0.0, and is actively maintained. It covers the same API. This project is not first and does not claim to be.

The two are shaped differently, so which one fits depends on what you are doing.

Use theirs if you want breadth. It exposes more of the API — registers, exemptions, UK establishments, officer disqualifications — and, importantly, it can download the filed documents themselves. This one deliberately does not: the Companies House document API is out of scope here.

Use this one if you are screening rather than browsing. The differences that matter:

Batch screening

screen_companies takes up to 50 names or numbers and returns one row each. Nothing else here does this.

Never guesses a company number

Retrieval tools refuse a company name outright, before any request. Given a name, a model produces a number that looks right, and a plausible wrong number returns a different real company that nothing downstream flags. ADR 5.

Signals, not scores

Facts read off the register with the date or name behind each, and deliberately no rating. ADR 7 has the argument.

Nothing dropped quietly

Partial results are labelled; a screening table that comes back short always says why. ADR 8.

Documentation that cannot go stale

The tool reference is generated from the running server and every example executes; CI fails if either drifts. ADR 9.

A tool-selection eval

Asks a real model which tool it reaches for, and fails on flakiness. ADR 10.

Eleven decisions are written up in docs/adr, including the ones that did not go the obvious way.

Install

npx -y companies-house-screening-mcp

Host configuration:

{
  "mcpServers": {
    "companies-house": {
      "command": "npx",
      "args": ["-y", "companies-house-screening-mcp"],
      "env": { "COMPANIES_HOUSE_API_KEY": "your_key" }
    }
  }
}

Or with Docker — note -i and no -t, because a TTY corrupts the JSON-RPC framing:

docker run --rm -i -e COMPANIES_HOUSE_API_KEY=your_key ghcr.io/kaylum54/companies-house-screening-mcp

Get a free API key at developer.company-information.service.gov.uk: register, create an application against the Live environment, and create a key of type REST (a stream key authenticates the same way but is for a different service).

What it is for

Two cases it was actually built around. More in docs/recipes, which are executed on every build.

Screening a supplier list

The quarterly onboarding list lands: forty new suppliers, first invoices due in a fortnight. One call.

{
  "tool": "screen_companies",
  "arguments": { "companies": ["04138203", "Bramble Facilities", "SC443221"] }
}

One row per company with its signal codes. Skip the rows carrying nothing; open the ones with accounts_overdue or outstanding_charges using company_snapshot.

The behaviour that earns its place is what happens to the awkward entries. "Bramble Facilities" matches several companies, so it comes back under unresolved with its candidates rather than resolved to a best guess. And if the rate limit runs out at company 31, the remaining nine come back under not_screened with the reset time — because a table that quietly stops at 31 looks exactly like a table where nine companies were clean.

Officers are excluded by default (one extra request per company), so the officer-based signals cannot appear unless you ask for them. sections_used says so on every response.

Verifying an invoice before paying it

A first invoice from an unfamiliar supplier, or an email asking to change bank details on an existing account.

{
  "tool": "company_snapshot",
  "arguments": { "company_number": "04138203" }
}

Four checks in one response: the company exists, status is active, registered_office_address matches what is printed on the invoice, and age_years plus the incorporated_within_last_year signal say whether this counterparty existed a month ago. The address is flattened to one line specifically so it can be compared without reassembling nine fields.

This is the case the no-names rule is for. Invoice-redirection fraud works because the paperwork looks right. If a tool accepts a company name and a model invents a number to look it up, you get a confident "verified — active, good standing" about a different real company, with real directors and real filings, and nothing anywhere flags it. You would have used a verification step to approve the fraud.

So passing a name is refused before any request is made:

{
  "error": {
    "code": "INVALID_COMPANY_NUMBER",
    "message": "\"Royal Mail Group Limited\" looks like a company name, not a company number.",
    "next_step": "Call find_company with this name to get candidate company numbers, then call this tool again with the number of the right one. Do not guess a number: a plausible wrong company number returns a real company and nothing will flag it as the wrong one."
  }
}

What it cannot tell you: whether a bank account belongs to that company. This raises or lowers suspicion; it does not settle it.

Why another API wrapper

The obvious way to build this is one MCP tool per endpoint. Twenty-two thin pass-throughs, a weekend's work, and it is what most published MCP servers are. It is also bad in three specific ways:

  • Every tool schema sits in the model's context on every turn, whether the task needs it or not.

  • It pushes the orchestration onto the model. "Is this supplier safe to onboard" becomes search, then profile, then officers, then charges, then insolvency — five round trips and five chances to lose the thread.

  • Companies House payloads carry structure no model reads — links, etag, kind, per-item ETags, filing-transaction arrays, nine-key address objects. Shaping them away saves between 36% and 72% depending on the endpoint, measured against real recorded responses rather than assumed (npm run measure).

So this server exposes eleven tools shaped around questions, two of which (company_snapshot and screen_companies) do the fan-out server-side and return one derived object. Retrieval tools accept a company number and refuse a company name, because given a name a model will guess a number, and a plausible wrong company number returns a real company that nothing downstream flags as wrong.

The tools

Tool

Returns

find_company

Ranked candidates for a name or number, with a disambiguation_needed flag.

find_officer

Candidate officer IDs for a person's name, with appointment counts.

get_company

Profile, plus derived flags for overdue filings, charges, insolvency and recent incorporation.

get_officers

Current and resigned officers, each with the ID needed to look up their other companies.

get_filing_history

What was filed and when, filterable by category.

get_charges

Secured debt, with a derived outstanding_count the API never reports.

get_psc

Who actually controls the company, and how that control is held.

get_insolvency

Insolvency cases and the practitioners appointed.

get_officer_appointments

Every company an officer sits on — the conflict-of-interest tool.

company_snapshot

Profile, officers, charges and insolvency in one call, with signals.

screen_companies

Up to 50 companies in, one row each out, nothing dropped quietly.

Full reference: docs/tools. Worked examples: docs/recipes — supplier screening, director conflict checks, invoice verification, debtor risk, competitor filing watch.

The signals are facts, not a rating. This server does not score companies and will not tell you whether one is safe to trade with — it reports what it found on the register, with the date or the name behind each observation, and leaves the judgement with the person who has the context. An empty signal list means nothing on the list was found, not that the company is sound. ADR 7 has the full reasoning.

Every tool is annotated readOnlyHint: true, publishes an output schema, and takes verbose to return the untouched payload alongside the shaped one.

Under the tools

Piece

What it does

loadConfig

Validates every environment variable at startup and reports all the problems at once, naming the variable rather than the internal field.

CompaniesHouseClient

Basic-auth requests, per-request timeout, jittered retry on 429 and 5xx, conditional revalidation, stale-on-failure fallback.

RateLimiter

Sliding window sized to the documented 600 per five minutes, with a safety margin and serialised acquisition.

ResponseCache

Memory over disk, TTL per resource kind, atomic writes, corrupt entries treated as a miss.

CompaniesHouseError

Every failure carries a stable code, a plain sentence and a next step.

Projections

Upstream read defensively field by field; output validated strictly against the published schema.

284 tests, no network, no API key required to run them.

Configuration

Only one variable is required.

Variable

Default

Notes

COMPANIES_HOUSE_API_KEY

Required. Create a REST API key at the developer portal. Not a streaming key.

CH_API_BASE_URL

https://api.company-information.service.gov.uk

Override for a proxy.

CH_RATE_LIMIT

600

Requests per window. Lower it if the key is shared with another process.

CH_RATE_WINDOW_MS

300000

Five minutes.

CH_RATE_SAFETY_MARGIN

0.95

Fraction of the budget this process will use.

CH_CACHE_ENABLED

true

CH_CACHE_DIR

platform cache dir

Respects XDG_CACHE_HOME and LOCALAPPDATA.

CH_TIMEOUT_MS

10000

Per request.

CH_MAX_RETRIES

3

Retries after the first attempt.

CH_LOG_LEVEL

info

error, warn, info or debug. Logs go to stderr.

CH_ENV_FILE

Absolute path to a .env for the server to read. Not set by default, deliberately.

Development

npm install
npm test
npm run typecheck
npm run build
npm run docs:generate

The documentation is generated and gated. docs/tools is rendered from the running server over a real MCP client, and every call in docs/recipes is executed when the pages are built. npm run docs:check fails if what is committed differs, CI runs it before the tests, and the suite runs the same comparison so the failure arrives while you still have the change in front of you. Change a tool description and you regenerate, or the build goes red.

The suite runs offline against fixtures recorded from the live Companies House API, so a fresh clone works with nothing configured. npm run record-fixtures re-records them — see tests/fixtures/README.md for which companies they come from and why those were chosen.

Once you hold a key, copy .env.example to .env and fill it in:

npm run test:live

Every development command reads that file. Anything already set in your shell wins over it. The published server does not read a .env unless CH_ENV_FILE names one — a host launches it with the host's working directory, and picking up whatever .env happens to be there is a good way to load the wrong credentials.

That test runs nightly in CI. Its job is not to pass — it is to fail loudly the week Companies House changes a field, so the fixtures get refreshed before a user finds the drift instead.

The tool-selection eval

Every test in this repository asks does the tool work. One thing none of them can ask is whether a model reaches for the right tool when a person asks a real question — a tool can be correct, fast and fully covered and still never get chosen, because its description is vague or overlaps another. That is the most common real defect in published MCP servers.

npm run eval -- --repeat 3

Runs through OpenRouter or the Anthropic API — set OPENROUTER_API_KEY or ANTHROPIC_API_KEY. It defaults to z-ai/glm-5.2 on OpenRouter, about 4p for a full pass, because an eval nobody runs because of the bill is not doing anything. Point --model at anything with tool support to compare.

Fourteen questions phrased the way a person would phrase them, scored on which tool was called first, whether a forbidden tool was touched, whether the arguments were right, and — the one that matters — whether the model invented a company number that was not in the question. A case that passes two runs in three is reported as flaky and fails, because intermittent selection means two descriptions overlap.

Run across three models (GLM 5.2, Kimi K3, DeepSeek V4 Pro) it scores 93–98%. The grounding group — given a company name and no number, search rather than recall one — passes 7/7 on all three. The failures clustered, and three of them turned out to be defects in my own tool descriptions and one in the eval itself, rather than in any model.

No Companies House key is needed; nothing is executed. Full comparison and what it found in evals/README.md, reasoning in ADR 10.

Design notes

Eleven decisions are written up in docs/adr:

  1. Recording architecture decisions

  2. The sliding-window rate limiter and its safety margin

  3. Errors as data rather than exceptions

  4. Caching, TTLs and the stale fallback

  5. Question-shaped tools, and why a name is refused

  6. Why the result payload is sent twice

  7. Signals, not scores

  8. Partial results, and never dropping anything quietly

  9. Generated documentation, gated in CI

  10. The tool-selection eval

  11. Tag-driven releases, signed with provenance

Scope

Read-only, permanently. Every tool is annotated readOnlyHint: true and there is no write path. The Companies House filing API, which submits documents on a company's behalf, is a different product with a different risk profile and is out of scope for this one. The streaming API is out of scope too. Fetching the PDF or iXBRL of a filing through the document API is phase 7 and would remain read-only.

Roadmap

Phase

Content

Status

1

Client, auth, rate limiter, cache, error mapping, fixtures

done

2

Nine primitive tools with Zod schemas and shaped projections

done

3

company_snapshot and screen_companies

done

4

Generated tool docs with a CI drift check, five worked recipes

done

5

Tool-selection eval suite, live smoke test in CI, remaining ADRs

done

6

npm and Docker release with provenance

done — published 2026-08-20

Licence

Source code: MIT.

Data returned by this server is published by Companies House under the Open Government Licence v3.0 and is not covered by the MIT licence. If you redistribute it, carry the attribution the OGL requires:

Contains public sector information licensed under the Open Government Licence v3.0.

This project is not affiliated with or endorsed by Companies House.

-
license - not tested
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • Companies House MCP — UK statutory company registry (BYO key)

  • Remote MCP server to enrich company profiles with structured B2B data and confidence scores.

  • Company intelligence via UK Companies House and risk screening across 386 risk data sources.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kaylum54/companies-house-screening-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server