companies-house-screening-mcp

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.

Category
访问服务器

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.

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选