companies-house-screening-mcp
MCP server for screening UK companies against the Companies House register, offering batch supplier screening, company snapshots, and factual signals rather than risk scores, with tools that never guess company numbers and documentation generated from the running server.
README
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.
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:
- Recording architecture decisions
- The sliding-window rate limiter and its safety margin
- Errors as data rather than exceptions
- Caching, TTLs and the stale fallback
- Question-shaped tools, and why a name is refused
- Why the result payload is sent twice
- Signals, not scores
- Partial results, and never dropping anything quietly
- Generated documentation, gated in CI
- The tool-selection eval
- 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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。