@whiteintel/mcp-server
Advanced tools
+265
-0
| # Changelog | ||
| ## 0.7.4 — 2026-08-11 — the tool a human can actually finish (20 → 21), and nine descriptions that were overstating | ||
| Two things shipped here. One is a missing tool. The other is the harder half: this package has | ||
| twice shipped a description that lied to an installing agent — a `find_similar` blurb saying a | ||
| working tool was broken, and an identifier scheme that resolved to zero rows — so every one of the | ||
| now-21 descriptions was re-read against a live production response before this version was cut. | ||
| Nine of them were wrong. Every number below came out of a command whose output is in the session | ||
| log; nothing here was inferred from a route file. | ||
| ### The gap: `get_payment_link` existed on the hosted surface and not in the package | ||
| Measured first, both surfaces, same minute: | ||
| ``` | ||
| POST https://whiteintel.dev/api/mcp {"method":"tools/list"} -> 21 tools | ||
| npx -y github:Hei33enberg/WhiteIntel-OS (initialize+tools/list) -> 20 tools, | ||
| serverInfo.version 0.7.3 | ||
| diff = exactly one name: get_payment_link | ||
| ``` | ||
| Then the endpoint it needs, because you do not advertise a tool pointing at something you have not | ||
| called: | ||
| ``` | ||
| GET https://whiteintel.dev/api/public/paylinks | ||
| -> HTTP 200, Cache-Control: public, max-age=3600 | ||
| -> { links: [standard:25, standard:5, standard:single], how_to_use: [4 strings] } | ||
| ``` | ||
| It answers, so the tool ships. Note what that body does **not** contain: any premium link. The | ||
| hosted description implies the links cover "the one-off dossiers"; measured, they cover Standard | ||
| only. This package says so, and says Premium still needs `buy_dossier`. | ||
| Why this is not a parity nit. 100% of the 205,497 purchase offers this company has ever served went | ||
| to machine callers with no card. `buy_dossier` hands back a Checkout Session that is single-use and | ||
| dead in 24 hours — worthless in a report a human opens tomorrow. `get_payment_link` returns a URL | ||
| that still works next week. That is the difference between an offer and a sale, and for four | ||
| releases it was reachable over HTTP and missing from the package every local client installs. | ||
| `get_pricing` gained the hosted surface's step 0 (no human at the keyboard → use | ||
| `get_payment_link`), and its `api.metered` line was aligned with the hosted payload, which the two | ||
| surfaces disagreed about: this one billed search as metered, the hosted one calls search, entity | ||
| lookup and MCP free. | ||
| ### `max_depth` is a request, not a promise — and four descriptions were selling it as a promise | ||
| The worst finding. Anonymously, against production, 2026-08-11: | ||
| ``` | ||
| /api/public/ownership-path?root=<barclays>&max_depth=6 -> max_depth: 2, depth_capped: true, plan: free | ||
| /api/public/graph/path?...&max_depth=4 -> max_depth: 2, depth_capped: true, plan: free | ||
| /api/public/graph/neighbourhood?...&depth=3 -> depth: 2, depth_capped: true, plan: free | ||
| /api/public/offshore-exposure/<barclays>?max_depth=6 -> depth_walked: 1, depth_capped: true, | ||
| verdict: checked_full_clean | ||
| ``` | ||
| The free plan caps every walk at two hops. Nothing in the old descriptions said so; they documented | ||
| the argument's range (up to 10 hops on `trace_ownership_path`) as if the caller got what it asked | ||
| for. An agent that asks for six hops to the UBO, receives two, and reads no further will write the | ||
| name of an intermediate holding company into a due-diligence report as the beneficial owner. All | ||
| four descriptions now tell the caller to read the RETURNED `depth` / `max_depth` / `depth_walked` | ||
| and `depth_capped`, and to scope its sentences to that number. | ||
| `check_offshore_exposure` needed a second correction on top. The old text mapped | ||
| `checked_full_clean` to "walked the full chain" and `depth_capped` to "the plan shortened the walk". | ||
| Measured, the two co-occur: the walk above ended at one hop — genuinely out of chain, below the | ||
| two-hop ceiling — and still reported `depth_capped: true`. So `depth_capped` means *a cap was in | ||
| force*, not *the cap bit*, and `checked_full_clean` is not a licence to write "no offshore | ||
| exposure" without quoting `depth_walked`. | ||
| ### `graph_neighbourhood` returns every edge twice at depth ≥ 2, and the payload does not say so | ||
| Four roots, `depth=2`, raw edges vs distinct on `from|to|predicate`: | ||
| ``` | ||
| BARCLAYS PLC 59 raw / 33 distinct | ||
| BARCLAYS BANK PLC 133 raw / 91 distinct | ||
| Barclays Bank Uk 48 raw / 35 distinct | ||
| GAZPROM (U.K.) 4 raw / 2 distinct | ||
| depth=1 on the same roots: 25/25, 26/26, 7/7 — clean | ||
| ``` | ||
| `edge_count` counts the duplicated list, and the duplicates are charged against the `edges` budget, | ||
| so a call can report `truncated: true` while holding far fewer distinct edges than the budget you | ||
| set. A database fix is in flight and is deliberately **not** described here — the description states | ||
| today's behaviour and tells the caller to de-duplicate before counting relationships. What the old | ||
| description got right is confirmed: with `edges=10` the response did come back `truncated: true` | ||
| with a plain-language `truncation_note`. | ||
| One claim we could **not** reproduce, and therefore do not repeat: a reported silent downgrade of | ||
| depth 2 to depth 1 with `depth_capped: false`. Six consecutive `depth=2` calls on the densest root | ||
| returned `depth: 2` with edges at depth 2 present every time. It may be load-dependent. The | ||
| instruction to read the returned `depth` stands on its own without it. | ||
| ### The rest, each measured | ||
| - **`get_pulse`** named the wrong registry. Its `ownership` stream was documented as GLEIF; | ||
| the newest 100 rows were **100% `borme`** — Spain's Boletín Oficial del Registro Mercantil. And | ||
| the old text insisted the default feed "is NOT ownership-only": the newest 100 rows of the | ||
| unfiltered feed were **100% `ownership`**, because the feed is ordered by ingest recency and | ||
| whichever loader ran last fills its head. Citation coverage, the one thing that description got | ||
| right, held everywhere: 100/100 rows carried a `source_url` on all four kinds. | ||
| - **`get_sanctions`** is not sanctions-only despite the name. BARCLAYS BANK PLC returned | ||
| `sanctioned: false` **with** a HIGH-severity `signal_type: 'crime'` row (a criminal/wanted | ||
| listing from `opensanctions_crime`, reaching it via its cluster) — and `list` and `regime` were | ||
| null on it, so a null `list` is not missing data. A `sanctioned: false` response can carry an | ||
| adverse finding you are obliged to report. | ||
| - **`semantic_search` / `find_similar`** were sold as general meaning-based corpus search. The | ||
| endpoint's own `coverage` object: **990,055 embedded of a 47,486,969 universe (2.1%), ~99.6% | ||
| risk-listed, ~97% natural persons.** In practice it is a risk-list search — "sanctioned russian | ||
| aluminium holding" returned sanctioned Russian *ships* as its top hits, and `find_similar` on | ||
| BARCLAYS BANK PLC returns `count: 0`. Both descriptions now say which slice they search, and that | ||
| empty means "not embedded", never "no peers exist". `semantic_search` also carries its measured | ||
| 6.4 s cold latency. | ||
| - **`get_financials`** called balance-sheet coverage "broad". Sampling 48 UK company entities from | ||
| `search_entities`: **11 returned any filed period.** The other 37 answered HTTP 200 with an empty | ||
| `financials` and a `note` — BARCLAYS BANK PLC (CH 01026167) among them. `get_company_details` | ||
| came out of the same sample at **45 of 48**, so it now carries that number as a contrast. | ||
| - **`resolve`** documented `confidence: 'name'` without saying what it costs you. The query | ||
| "Tesco" resolved to a **French** company literally named TESCO (`fr-siren:454067281`), while | ||
| `gb-coh:00445790` resolved 'exact' to TESCO PLC. A 'name' hit is a candidate. Unmatched rows come | ||
| back `{ match: null, confidence: null }`; the 25-item anonymous limit was verified by a 26th item | ||
| returning HTTP 400. | ||
| - **`lookup_by_identifier`** — all eleven schemes were exercised against production and every one | ||
| resolved a real entity, `lei` and `uen` included (both needed values pulled from the corpus | ||
| first; a format-valid LEI that we simply do not hold 404s, which is how the earlier `nip` ghost | ||
| hid). The description now separates the two failure modes an agent will otherwise conflate: | ||
| unsupported scheme → **400** with the accepted set in `detail`; supported scheme, unheld value → | ||
| **404**. `cy-reg` → 400 verified; `cusip:` appears on US rows and is likewise not accepted. | ||
| - **`search_entities`** pointed at `get_entity (registry_profile)` for provenance. It is populated | ||
| on **22 of 32** sampled entities, so the description now names the fallbacks | ||
| (`linked_records[].registry`, `connections[].source`). Its "full corpus" claim held: one name | ||
| search returned FR, BR and US rows across `siren`, `lei`, `br-cnpj` and `cusip`. | ||
| - **`trace_ownership_path`** promised "the ordered chain(s)". It returns one flat `hops` array. | ||
| Corpus figures re-read from `/api/public/stats` and left alone because they were right: 29 | ||
| registries, 116,163,032 entities, 21,452,620 relationships. | ||
| ### Guardrails | ||
| `test/smoke.mjs` asserted `tools.length >= 9`. A floor is how this package shipped 0.7.0 with two | ||
| tools missing and nothing failing. It now asserts the **exact** 21 names — so a rename surfaces as | ||
| a named diff rather than an unchanged count — that every tool carries a description and an | ||
| inputSchema, and that `serverInfo.version` equals `package.json`, the drift that produced 0.7.0 | ||
| vs 0.7.1 and a hosted surface reporting 0.7.0 while the package was 0.7.3. | ||
| ``` | ||
| $ npm run smoke | ||
| whiteintel-mcp-server running on stdio · 21 tools · API https://whiteintel.dev · anonymous (free tier) | ||
| OK: serverInfo.version 0.7.4 matches package.json | ||
| OK: tools/list returned 21 tools, all named and described | ||
| $ npm test | ||
| # pass 4 # fail 0 | ||
| ``` | ||
| Version bumped in all five places that can disagree: `package.json`, `package-lock.json`, | ||
| `server.json` (twice), `claude-plugin.json`, and the `Server()` constructor in `index.js` that the | ||
| client actually reads over the wire. | ||
| ## 0.7.3 — 2026-08-11 — bounded graph walk (2 new tools, 18 → 20) | ||
| The app-side API and the DB functions landed first (WhiteIntel migration 0295, LINEAR-4806); | ||
| this package describes them so the two surfaces stay in step. Both endpoints were measured live | ||
| against production before this version was cut, not assumed from the route files: | ||
| ``` | ||
| GET /api/public/graph/neighbourhood?root=<uuid>&depth=2&edges=120 -> 200 in 0.96s | ||
| GET /api/public/graph/path?from=<uuid>&to=<uuid>&max_depth=3 -> 200 (400 + a clear | ||
| message when from == to) | ||
| ``` | ||
| ### Install channel changed: `claude-plugin.json` and `mcp.json` now install from GitHub | ||
| Measured 2026-08-11: `npm view @whiteintel/mcp-server version` returns **0.7.0** while this source | ||
| tree is 0.7.3. Anyone installing through the plugin catalogue was therefore getting a package three | ||
| versions behind — 18 tools, no graph tools at all, and a `find_similar` description that told the | ||
| calling agent the tool was broken when it was not. | ||
| This package has no build step and the repository is public, so `github:` resolves to exactly this | ||
| code: | ||
| ``` | ||
| npx -y github:Hei33enberg/WhiteIntel-OS | ||
| ``` | ||
| Both manifests now point there. `server.json` still declares the npm package, because that is what | ||
| the MCP registry entry describes — switch every one of them back to `@whiteintel/mcp-server` on the | ||
| day `npm view` reports the current version. | ||
| ### Corpus figures corrected again | ||
| `index.js`, `server.json` and `claude-plugin.json` each claimed **30 registries**. Measured from | ||
| `/api/public/stats` at 2026-08-11 05:30Z: **29**. The entity count, ~116.2M, was already right | ||
| (116,163,032). This is the third consecutive release in which a hardcoded corpus number in a | ||
| manifest was wrong — the manifests are what the catalogue and the installer read, and they are the | ||
| files that keep getting missed. | ||
| - **`graph_neighbourhood`** — every ownership/control edge within N hops of an entity, in | ||
| BOTH directions. Fills the hole between `get_entity` (direct relationships only) and | ||
| `trace_ownership_path` (upward only). Depth 1–3, edge budget 10–300. | ||
| - **`graph_path`** — the ordered hops connecting two entities, or an explicit "not found | ||
| within these bounds". | ||
| **The bounds are in the database, not in this package.** Depth, edge budget and a | ||
| per-entity fan-out cap are constants inside the Postgres functions; no tool argument can | ||
| widen them. Measured on the fattest node in the corpus (22,420 edges on one entity): | ||
| a full-cap neighbourhood returns in 9.6 ms, and the caps hold even when the caller asks | ||
| for depth 99 and a million edges. This is deliberate history — `/api/public/similar` once | ||
| consumed 3.8 GB on a single call and answered 503 because its cost tracked the data | ||
| instead of the contract. | ||
| **`graph_path` reports a bounded negative, and says so.** At most 15 edges are followed | ||
| per entity, per direction, per hop. `found: false` means no path was found within those | ||
| bounds; it is not evidence that two entities are unconnected, and the payload carries | ||
| `exhaustive: false` plus a `bounds_note` so an agent cannot honestly write "no link" | ||
| into a report. A due-diligence tool that turns "we did not look far enough" into a clean | ||
| bill of health is worse than one that returns nothing. | ||
| ## 0.7.2 — 2026-08-11 | ||
| Descriptions, manifests and docs only — no behaviour change, no new tools (still 18). | ||
| - **Cyprus (DRCOR) is live, and the README says exactly how far it goes.** New source, | ||
| loading since 2026-08-11, licensed CC BY 4.0 (attribution carried in the README). | ||
| Scope is stated bluntly because it is easy to oversell: the Cypriot open data is the | ||
| **nominal layer — directors, secretaries, trade-name owners — with no shareholders and | ||
| no beneficial owners.** Every Cyprus edge measured is a `Directorship`; there is not one | ||
| ownership edge. Cypriot companies therefore answer `trace_ownership_path` / | ||
| `check_offshore_exposure` with `no_ownership_data`, which means "no ownership edges held", | ||
| not "cleanly owned". No row count is frozen into the docs while the load is still running. | ||
| - **`lookup_by_identifier` documents a trap it already had.** Cyprus records surface a | ||
| `cy-reg:` identifier, but the route hard-rejects that scheme with a 400 (verified against | ||
| the live API). The description now says so and points to `search_entities juris='cy'`. | ||
| The enum is unchanged — the fix belongs in the description, not the API. | ||
| - **`get_pulse` was wrong on three counts and is now measured.** It claimed `watchlist` was | ||
| "currently uncited (source-url NULL for every row)" and "filtered OUT of the default feed". | ||
| Measured: all 47,784 watchlist rows carry a source URL, and they do appear in the default | ||
| feed. It also called them "PEP listings" when they are PEP (33,685) + criminal/wanted | ||
| (9,584) + procurement debarment (4,515). And a fourth live kind, **`sanction`** (209 rows, | ||
| all cited), was missing from the enum, so agents could not filter for it even though the | ||
| API serves it. Added. | ||
| - **`search_entities` stops overpromising provenance.** It said "each hit is flagged with its | ||
| source"; the per-hit `source` field only distinguishes resolved corpus from live-registry | ||
| passthrough and never names the originating registry. Reworded, and it now points at | ||
| `get_entity` / `get_dossier` for real per-record citations. | ||
| - **GLEIF ownership relations now have a nightly refresh scheduled** (the stream had been | ||
| frozen since 2026-07-09). Stated as *scheduled*, not *verified*: the job is active, but | ||
| the loader ledger records no completed GLEIF pass yet, so `source_freshness()` still | ||
| reports the source as untracked. It will say otherwise once a pass lands. | ||
| - **Corpus counts corrected everywhere, including the two files the last pass missed.** | ||
| 0.7.1 claimed in its commit message to fix the count "everywhere it appears" but only | ||
| touched the README and `index.js`; `claude-plugin.json` and `server.json` kept shipping | ||
| ~102.3M / 29 and received nothing but a version bump. All four now read ~116.2M / 30, | ||
| measured 2026-08-11 from `/api/public/stats`. | ||
| - **Version drift fixed.** `index.js` still announced itself to clients as `0.7.0` while the | ||
| package was `0.7.1`. The wire version, `package.json`, `server.json` and | ||
| `claude-plugin.json` are now all `0.7.2`. | ||
| ## 0.7.1 — 2026-08-10 | ||
| - `find_similar` stopped telling every installing agent it was dead: the "TEMPORARILY | ||
| UNAVAILABLE … returns 503" note was true only while the ANN index was dropped, and the | ||
| index has been rebuilt as IVFFlat. The real limit is coverage (~1.9% of the corpus | ||
| embedded), and an entity outside it gets an empty list, not an error. | ||
| ## 0.7.0 — 2026-07-22 | ||
@@ -4,0 +269,0 @@ |
+139
-28
@@ -10,5 +10,8 @@ #!/usr/bin/env node | ||
| * | ||
| * Data: live UK Companies House (company + officer + PSC lookup) plus three | ||
| * fully-worked demo investigations. The offshore corpus (ICIJ Offshore Leaks | ||
| * et al.) is on the roadmap and is clearly flagged when absent. | ||
| * Data: ~130.7M entities fused across 31 public/semi-public registries — OpenOwnership, | ||
| * GLEIF, ICIJ Offshore Leaks, SEC EDGAR, Cyprus DRCOR, sanctions/PEP lists and more — | ||
| * cross-source resolved onto one cited identity spine, plus live UK Companies House | ||
| * lookups. Entity count read from /api/public/stats on 2026-08-16 (130,735,728, itself a | ||
| * planner estimate); the live figure is always at whiteintel.dev/api/public/stats, whose | ||
| * source map is rebuilt by counting registries — new sources appear there on their own. | ||
| * | ||
@@ -22,3 +25,5 @@ * Freemium. Without a key, calls hit the anonymous free tier. Set WHITEINTEL_API_KEY | ||
| * unlocks the paid depth (full UBO chain + financial history; premium adds | ||
| * itemised assets). Stdio transport. | ||
| * itemised assets). When there is NO human at the keyboard, get_payment_link | ||
| * returns permanent Stripe links that can be handed over asynchronously — a | ||
| * buy_dossier session is single-use and expires in 24h. Stdio transport. | ||
| * Add to an MCP client (Claude Desktop, Cursor) with: | ||
@@ -140,3 +145,7 @@ * { "command": "npx", "args": ["-y", "@whiteintel/mcp-server"], | ||
| if (!res.ok) { | ||
| const detail = body && typeof body === "object" ? (body.error || body.detail || body.message) : null; | ||
| // `message` first: when the service sends prose it is the actionable half. 422 | ||
| // insufficient_data, for instance, carries "no ownership links, company details or | ||
| // financials on file" — an agent can act on that (pick another entity) where the bare | ||
| // error code just looks like a fault. Falls back to the code when there is no prose. | ||
| const detail = body && typeof body === "object" ? (body.message || body.error || body.detail) : null; | ||
| if (res.status >= 500 || res.status === 429) { | ||
@@ -181,3 +190,3 @@ const ra = res.headers.get("retry-after"); | ||
| includes: | ||
| "Unlimited full-depth ownership graph, 10 Premium dossiers/mo included, risk scores + watchlists, metered API/MCP credit allowance.", | ||
| "Unlimited full-depth ownership graph + unlimited Premium dossiers, risk scores + watchlists, metered API/MCP credit allowance. Dossier PDF/JSON exports are capped at 100 per month — the dossiers themselves stay viewable at full depth without limit, and one-off €39/€99 dossier purchases are bought artifacts that never count against this cap.", | ||
| }, | ||
@@ -187,4 +196,6 @@ business: { | ||
| includes: | ||
| "Everything in Investigator with 3 seats, 75 Premium dossiers/mo included, monitoring + webhooks, larger API/MCP credit allowance.", | ||
| "Everything in Investigator with 3 seats, unlimited Premium dossiers, monitoring + webhooks, larger API/MCP credit allowance. Dossier PDF/JSON exports are capped at 500 per month — the dossiers themselves stay viewable at full depth without limit, and one-off €39/€99 dossier purchases are bought artifacts that never count against this cap.", | ||
| }, | ||
| export_quota: | ||
| "Subscriber dossier exports (PDF/JSON) are metered per month: Investigator 100/mo, Business 500/mo. Exceeding the monthly allowance returns HTTP 429 on the export call (retry after the cap resets on the billing month). One-off €39/€99 dossier purchases are bought artifacts and are NEVER counted against this quota.", | ||
| note: | ||
@@ -194,5 +205,9 @@ "Subscriptions are bought at whiteintel.dev/pricing (account required); the wi_ API key from Settings then lifts this MCP server's limits via WHITEINTEL_API_KEY.", | ||
| api: { | ||
| metered: "Pay-as-you-go API from €0.20/call, tapering to €0.12 and €0.08/call at volume.", | ||
| // Aligned 2026-08-11 with the hosted /api/mcp get_pricing payload (measured — the two | ||
| // surfaces disagreed: this one billed search as metered, the hosted one calls it free). | ||
| metered: | ||
| "Search, entity lookup and MCP are FREE and unlimited (the open language). DEPTH calls are pay-as-you-go from €0.20/call, tapering to €0.12 and €0.08/call at volume.", | ||
| }, | ||
| how_an_agent_buys: [ | ||
| "0. NO HUMAN AT THE KEYBOARD RIGHT NOW? Call get_payment_link instead of buy_dossier. It returns permanent, reusable Stripe links you can put in a report or a message — append ?client_reference_id=<entity uuid> to bind one to a company. A buy_dossier URL is single-use and expires in 24h, so it is the wrong artefact to hand over asynchronously. Measured 2026-08-11: payment links exist for the STANDARD tier only (single / 5 / 25); Premium still needs buy_dossier.", | ||
| "1. Call buy_dossier { tier, pack?, entity_id?, entity_name? } → returns a Stripe checkout_url.", | ||
@@ -236,3 +251,3 @@ "2. Open the checkout_url so a human (or a payment-capable agent) completes payment — no WhiteIntel account needed; Stripe collects an email for delivery.", | ||
| description: | ||
| "Search every node in the WhiteIntel corpus — companies AND people — by name, across live data and the seeded demo investigations (Meridian, Tideway, Ardent). Returns entity ids you then pass to get_entity or trace_ownership_path. Each hit is flagged with its source.", | ||
| "Search every node in the live WhiteIntel corpus — companies AND people — by name, across all fused sources. This is the lexical search and it always covers the FULL corpus, so it is the fallback whenever semantic_search comes back thin. Returns entity ids you then pass to get_entity or trace_ownership_path. Each hit's `source` says whether it came from the resolved corpus or a live registry passthrough — it does NOT name the originating registry. For that provenance call get_entity, whose `entity.registry_profile` names the source register when we hold one — measured 2026-08-11 it was populated on 22 of 32 sampled entities, so expect null sometimes and fall back to `linked_records[].registry` and `connections[].source` — or get_dossier, which cites per-record source URLs. Use `juris` to scope to a country (e.g. gb, ky, us, cy). Reach into the non-UK sources is verified, not assumed: a name search for 'PETROLEO BRASILEIRO' returned FR (siren), BR (lei and br-cnpj) and US (cusip) rows in one response, 2026-08-11.", | ||
| inputSchema: { | ||
@@ -281,3 +296,5 @@ type: "object", | ||
| description: | ||
| "Walk the ownership graph upward from a root entity, up to max_depth hops, and return the ordered chain(s) connecting it to the ultimate beneficial owner. Use this to answer 'who ultimately controls X?'. Get the root id from search_entities.", | ||
| "Walk the ownership graph upward from a root entity and return the ordered hops connecting it to the ultimate beneficial owner. Use this to answer 'who ultimately controls X?'. Get the root id from search_entities. " + | ||
| "THE HOP AT THE TOP OF THE LIST IS NOT NECESSARILY THE ULTIMATE OWNER, AND max_depth IS A REQUEST, NOT A PROMISE. Measured 2026-08-11 anonymously: max_depth=6 came back as `max_depth: 2, depth_capped: true, plan: 'free'` — the walk stopped two hops up and the payload said so only in those two fields. So before you name a UBO, compare `hop_count` with the RETURNED `max_depth` and check `depth_capped`: if the walk was capped and the topmost owner still has owners, you have found an intermediate holder, not the beneficial owner. A paid API key walks deeper. " + | ||
| "Shape: a single flat `hops` array (each hop from/fromName/to/toName/role/share/source), not one array per branch. `as_observed` is a standing caveat: edges carry the date we OBSERVED them in a registry, not a validity period — we hold no ownership end dates, so a link shown here may already have ended.", | ||
| inputSchema: { | ||
@@ -287,3 +304,3 @@ type: "object", | ||
| root: { type: "string", maxLength: 80, description: "Root entity id to trace from." }, | ||
| max_depth: { type: "number", minimum: 1, maximum: 10, description: "Max hops to walk (default 6)." }, | ||
| max_depth: { type: "number", minimum: 1, maximum: 10, description: "Max hops to request (default 6). The plan lowers it — anonymous callers measured at 2 — so trust the response's `max_depth` / `depth_capped`, not this value." }, | ||
| }, | ||
@@ -294,6 +311,54 @@ required: ["root"], | ||
| }, | ||
| // ── bounded graph walk (S13·P3, LINEAR-4806; DB migration 0295) ─────────────── | ||
| // Every bound is a CONSTANT inside the Postgres function, not a parameter of these | ||
| // tools. An agent can ask for less; there is no argument that asks for more. This is | ||
| // the direct lesson of /api/public/similar, which once consumed 3.8 GB on a single | ||
| // call and returned 503 because its cost was a function of the data, not of the | ||
| // contract. The descriptions state the bounds because an agent that reads | ||
| // `found: false` as "not connected" will write that sentence into a due-diligence | ||
| // report — and a bounded search has not earned that sentence. | ||
| { | ||
| name: "graph_neighbourhood", | ||
| description: | ||
| "Return every ownership/control edge within a bounded number of hops of one entity, in BOTH directions: who it controls, who controls it, and their neighbours. Use it to answer 'what sits around this company?' — the wider view that trace_ownership_path (upward only) does not give. Hard-capped in the database: depth 3, 300 edges, and at most 25 edges followed per entity per direction per hop. " + | ||
| "READ THE DEPTH FIELDS IN THE RESPONSE — DO NOT ASSUME YOU GOT THE DEPTH YOU ASKED FOR. There is no field called `depth` any more, and that rename is deliberate: the old `depth` was the CLAMPED REQUEST, never the depth walked, and it was being read as a promise. The response now carries `depth_requested` (what your plan allowed), `depth_walked` (measured off the returned edges' own hop numbers — the only depth that is actually proven), `depth_capped`, and `completeness`. Measured 2026-08-11 on an anonymous caller: depth=3 requested returned depth=2 with depth_capped=true, because the free plan caps every walk at 2 hops. Any sentence you write about what is or is not around this entity must be scoped to the RETURNED depth. " + | ||
| "EDGE COUNTS FELL BY UP TO 2.7x ON 2026-08-11 AND NOTHING WAS LOST — read this before you treat it as the corpus shrinking. Until that date the walk emitted the same edge two and three times at depth 2 or more, `edge_count` counted the duplicated list, and the duplicates were charged against your `edges` budget. Measured on identical requests before and after the fix: 72 -> 27, 29 -> 13, and at the maximum budget 300 rows holding 285 real edges -> 300 rows holding 300. So a call you made yesterday and repeat today can return far fewer edges for the same subject: the smaller number is the true one, and your budget now buys real edges. One consequence worth knowing: at depth 1 a root can drop from 4 edges to 2, because the registry genuinely holds rows that are identical in every field this endpoint returns and the response has no way to represent the difference. That is also a correction, not a loss. " + | ||
| "`truncated: true` plus a plain-language `truncation_note` does work and does mean the edge budget ran out (verified with edges=10); that is NORMAL for hub entities (the corpus holds single nodes with more than 22,000 edges) and means the picture is partial, not wrong. Each edge carries `origin`: 'registry' (observed in a source registry) or 'derived'/'curated'/'asserted' (inferred by WhiteIntel). Get the root id from search_entities or resolve.", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| root: { type: "string", maxLength: 80, description: "Root entity uuid." }, | ||
| depth: { type: "number", minimum: 1, maximum: 3, description: "Hops to walk (default 2). A REQUEST, not a guarantee — the plan caps it (anonymous callers measured at 2 hops) and the response's `depth_walked` is the authority — it is measured from the edges that came back, not echoed from your request." }, | ||
| edges: { type: "number", minimum: 10, maximum: 300, description: "Edge budget (default 120). Lower it for a legible picture, raise it for completeness." }, | ||
| }, | ||
| required: ["root"], | ||
| }, | ||
| handler: (a) => apiGet(`/api/public/graph/neighbourhood${qs({ root: a.root, depth: a.depth ?? 2, edges: a.edges ?? 120 })}`), | ||
| }, | ||
| { | ||
| name: "graph_path", | ||
| description: | ||
| "Find how two entities are connected: a bounded breadth-first search over ownership and control edges in both directions, returning the ordered hops from one to the other. WARNING, AND IT CHANGES HOW YOU MUST REPORT THE RESULT: this search is BOUNDED, NOT EXHAUSTIVE. At most 15 edges are followed per entity, per direction, per hop, so a genuine connection running through a heavily-connected intermediary can be missed. `found: false` means NO PATH WAS FOUND WITHIN THOSE BOUNDS — it is NOT evidence that the two entities are unconnected, and must never be reported as a clean result. The response always carries `exhaustive: false`, a structured `verdict` (e.g. 'connected_within_bounds') and a `bounds_note` restating this. " + | ||
| "AND THE DEPTH YOU GET IS NOT THE DEPTH YOU ASK FOR: the response echoes its own `max_depth` plus `depth_capped`, and those are the authority. Measured 2026-08-11 anonymously — max_depth=3 and max_depth=4 both came back as `max_depth: 2, depth_capped: true, plan: 'free'`. So a free-tier `found: false` is a two-hop negative however many hops you requested; say two hops, not four.", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| from: { type: "string", maxLength: 80, description: "Start entity uuid." }, | ||
| to: { type: "string", maxLength: 80, description: "End entity uuid." }, | ||
| max_depth: { type: "number", minimum: 1, maximum: 4, description: "Max hops to request (default 3). Reduced by the plan — anonymous callers measured at 2 — so read the response's `max_depth` and `depth_capped`. Depth 4 is measurably slower on densely connected entities; request it deliberately." }, | ||
| }, | ||
| required: ["from", "to"], | ||
| }, | ||
| handler: (a) => apiGet(`/api/public/graph/path${qs({ from: a.from, to: a.to, max_depth: a.max_depth ?? 3 })}`), | ||
| }, | ||
| { | ||
| name: "lookup_by_identifier", | ||
| description: | ||
| "Resolve an entity by a strong external identifier instead of a name — a LEI, OFAC SDN uid, EU/UN/UK sanctions id, Singapore UEN, Polish NIP, SEC CIK, Polish KRS, or UK Companies House number. Returns the single resolved entity (id, type, jurisdiction, identifier, risk) so you can pivot into get_entity / get_dossier / get_sanctions. Use this when you already hold a registry id and want the corpus node behind it.", | ||
| "Resolve an entity by a strong external identifier instead of a name — a LEI, OFAC SDN uid, EU/UN/UK sanctions id, Singapore UEN, SEC CIK, Polish KRS, UK Companies House number, French SIREN, or Brazil RFB CNPJ. Returns the single resolved entity (id, type, jurisdiction, identifier, risk) so you can pivot into get_entity / get_dossier / get_sanctions. Use this when you already hold a registry id and want the corpus node behind it. All eleven schemes were exercised against production on 2026-08-11 and every one resolved a real entity — no scheme in this enum is decorative. " + | ||
| "DISTINGUISH THE TWO FAILURE MODES: an unsupported scheme returns HTTP 400 with `error: 'bad_request'` and the accepted set spelled out in `detail`, whereas a supported scheme whose value we simply do not hold returns HTTP 404 `error: 'not_found'`. A 404 is a statement about the corpus, not about the tool — fall back to search_entities. " + | ||
| "NOT every identifier you may see in a response is resolvable here — the enum below is the complete accepted set and the route hard-rejects anything else with a 400. In particular Cyprus records carry a `cy-reg:` identifier that this tool does NOT accept (verified: `cy-reg` → 400), and neither is the `cusip:` seen on US securities rows: reach Cypriot companies with search_entities using juris='cy'.", | ||
| // LINEAR-5147: `nip` removed — no Polish NIP is stamped onto entities.identifier today | ||
| // (they live only in props->>'nip' on `krs:` rows), so the scheme resolved to nothing. | ||
| // Keep this enum aligned with app IDENTIFIER_SCHEMES; the server-side by-identifier | ||
| // route hard-rejects unknown schemes. | ||
| inputSchema: { | ||
@@ -304,4 +369,4 @@ type: "object", | ||
| type: "string", | ||
| enum: ["lei", "ofac", "eu", "un", "uk", "uen", "nip", "sec", "krs", "gb-coh", "siren"], | ||
| description: "Identifier scheme: lei | ofac | eu | un | uk | uen | nip | sec | krs | gb-coh | siren (French SIREN, 9 digits).", | ||
| enum: ["lei", "ofac", "eu", "un", "uk", "uen", "sec", "krs", "gb-coh", "siren", "br-cnpj"], | ||
| description: "Identifier scheme: lei | ofac | eu | un | uk | uen | sec | krs | gb-coh | siren (French SIREN, 9 digits) | br-cnpj (Brazil RFB CNPJ; accepts 8-digit root or full 14-digit form 12.345.678/0001-95).", | ||
| }, | ||
@@ -317,3 +382,5 @@ value: { type: "string", maxLength: 100, description: "The identifier value (e.g. an LEI, an OFAC SDN uid, a Companies House number)." }, | ||
| description: | ||
| "Return an entity's sanctions exposure: every 'sanctioned' risk signal (OFAC SDN, EU, UN, UK lists) for the entity AND its resolved cluster siblings — each with the list, regime, source list and a source URL. Tells you whether an entity, or anything cross-source-resolved to the same real-world party, is on a public sanctions list. Get the id from search_entities or lookup_by_identifier.", | ||
| "Return an entity's screening exposure for the entity AND its resolved cluster siblings, each with a source URL. " + | ||
| "IT IS NOT SANCTIONS-ONLY, DESPITE THE NAME — read each row's `signal_type`. Measured 2026-08-11: BARCLAYS BANK PLC came back `sanctioned: false` with one signal of `signal_type: 'crime'` (severity HIGH, source_list `opensanctions_crime`, a criminal/wanted listing reaching it via its cluster). Only `signal_type: 'sanctioned'` rows are sanctions designations, and only those reliably carry `list` and `regime` — on the crime row both were null, so do not read a null `list` as missing data. Two consequences: a `sanctioned: false` response can still contain a HIGH-severity adverse finding you must report, and 'no sanctions signal' (what the top-level flag and `note` describe) is not 'nothing found'. " + | ||
| "Response splits the top-level flag: `sanctioned_self` = a direct listing ON this entity; `sanctioned_via_cluster` = the flag reaches it ONLY via a cross-source cluster sibling (~2.3% false-positive tail on UK OpenOwnership resolution — treat cluster-only hits as a lead until you verify the sibling really is the same real-world party). The aggregate `sanctioned` (self OR cluster) is preserved for back-compat. Get the id from search_entities or lookup_by_identifier.", | ||
| inputSchema: { | ||
@@ -331,3 +398,4 @@ type: "object", | ||
| description: | ||
| "Walk the ownership chain upward from an entity and flag, hop by hop, whether each node is sanctioned and/or sits in a secrecy jurisdiction (classic tax-haven / offshore-secrecy country). Returns the chain, a boolean `exposed`, and the flagged hops — the offshore-layering lead behind 'does this entity sit on a sanctioned or secrecy-jurisdiction ownership chain?'. Get the id from search_entities or lookup_by_identifier.", | ||
| "Walk the ownership chain upward from an entity and flag, hop by hop, whether each node is sanctioned and/or sits in a secrecy jurisdiction (classic tax-haven / offshore-secrecy country). Returns the chain, the flagged hops, and a structured 4-state `verdict` — BRANCH ON `verdict`, NOT on `exposed`. States: `no_ownership_data` (we hold zero ownership edges from this entity — NOT a clean verdict, exposure cannot be evaluated), `flagged` (a sanctioned or secrecy-jurisdiction hit sits on the walked chain), `checked_to_max_depth_truncated` (walk reached the depth cap with more chain above — a flagged owner may still sit higher, NOT clean), `checked_full_clean` (the walk ran out of chain before the cap, no flag). Also returns `depth_walked` (how deep the walk actually reached) and `depth_capped`. " + | ||
| "READ `depth_capped` EVEN WHEN THE VERDICT IS `checked_full_clean`, because the two co-occur. Measured 2026-08-11 anonymously with max_depth=6: `verdict: 'checked_full_clean', depth_walked: 1, depth_capped: true, plan: 'free'`. `depth_capped: true` means A CAP WAS IN FORCE, not that the cap necessarily bit — here the chain genuinely ended after one hop, below the free plan's 2-hop ceiling. The honest report of that response is 'clean over the one hop of ownership we hold, on a walk a free key limits to two', which is what the payload's own `note` says in prose. Never promote `checked_full_clean` to 'no offshore exposure' without quoting `depth_walked`. Anonymous callers walk at most 2 hops however high you set max_depth. Legacy `exposed` boolean is retained but is only meaningful when `verdict='flagged'`. Get the id from search_entities or lookup_by_identifier.", | ||
| inputSchema: { | ||
@@ -337,3 +405,3 @@ type: "object", | ||
| id: { type: "string", maxLength: 80, description: "Entity id to assess." }, | ||
| max_depth: { type: "number", minimum: 1, maximum: 6, description: "Max ownership hops to walk (default 6)." }, | ||
| max_depth: { type: "number", minimum: 1, maximum: 6, description: "Max ownership hops to request (default 6). Anonymous callers are capped at 2 — read `depth_walked` in the response." }, | ||
| }, | ||
@@ -347,3 +415,3 @@ required: ["id"], | ||
| description: | ||
| "Companies House register detail for a UK company by entity id: registered address, status, company type, incorporation date, SIC industry codes, and the filing/compliance layer — accounts type, last-filed and next-due dates (flagged when OVERDUE), confirmation-statement status, outstanding mortgage charges, and former ('also known as') names. Use this for 'where is X registered / what does it file / is it overdue / what was it called before'. Get the id from search_entities or lookup_by_identifier.", | ||
| "Companies House register detail for a UK company by entity id: registered address, status, company type, incorporation date, SIC industry codes, and the filing/compliance layer — accounts type, last-filed and next-due dates (flagged when OVERDUE), confirmation-statement status, outstanding mortgage charges, and former ('also known as') names. Use this for 'where is X registered / what does it file / is it overdue / what was it called before'. Returns `{ entity, company_details, provenance, note, source }` — this is the best-populated of the UK detail tools, measured 2026-08-11 at 45 of 48 sampled UK company entities carrying a non-empty `company_details` (contrast get_financials at 11 of the same 48). Get the id from search_entities or lookup_by_identifier.", | ||
| inputSchema: { | ||
@@ -359,3 +427,4 @@ type: "object", | ||
| description: | ||
| "Filed financial figures for a UK company by entity id, year-over-year, from Companies House iXBRL accounts: turnover, profit/(loss), net assets, cash, shareholder funds, fixed/current assets, and employee count per reporting period. Use this for 'what are X's revenue / profit / net assets / how many employees'. Coverage is uneven — balance-sheet items and employees are broad, but turnover/profit are sparse because micro-entities file no profit-and-loss account. Get the id from search_entities.", | ||
| "Filed financial figures for a UK company by entity id, year-over-year, from Companies House iXBRL accounts: turnover, profit/(loss), net assets, cash, shareholder funds, fixed/current assets, and employee count per reporting period. Use this for 'what are X's revenue / profit / net assets / how many employees'. Returns `{ entity, financials, note, source }`. " + | ||
| "MOST ENTITIES HAVE NOTHING HERE, AND THAT IS THE NORMAL ANSWER, NOT AN ERROR. Measured 2026-08-11 on a sample of 48 UK company entities drawn from search_entities: only 11 returned any filed period — the other 37 came back HTTP 200 with an empty `financials` and a `note` saying so (even BARCLAYS BANK PLC, CH 01026167, has none loaded). Earlier versions of this description called balance-sheet coverage 'broad'; it is not. Within the accounts we DO hold, the per-field skew is real: balance-sheet items and employee counts are the well-populated ones, while turnover and profit are sparse because micro-entities file no profit-and-loss account. Read `note` before writing 'no revenue' — absent filings and a filed zero are different claims. Get the id from search_entities.", | ||
| inputSchema: { | ||
@@ -371,7 +440,8 @@ type: "object", | ||
| description: | ||
| "The WhiteIntel Pulse activity feed: recent ownership / control changes across the corpus, newest first, each with a source registry. Use this to answer 'what changed recently / any recent ownership movements' or to monitor the corpus. Optional kind filter (ownership | officer | filing | sanction | asset | status).", | ||
| "The WhiteIntel Pulse activity feed: recent corpus events — ownership/control changes, filed accounts, watchlist and sanctions designations — newest first, each with a source registry. Use this to answer 'what changed recently' or to monitor the corpus. All four kinds are live and all four are cited: measured 2026-08-11, 100 of 100 rows carried a source_url for every kind. " + | ||
| "THE UNFILTERED FEED IS NOT A BALANCED MIX. It applies no kind filter, but it is ordered by ingest recency, so whichever loader ran last fills the head of it. Measured 2026-08-11: the newest 100 rows of the default feed were 100% `kind: 'ownership'`, all from one registry. If you want a particular stream — or any sense of proportion between them — pass `kind` and do not infer 'nothing else happened' from the default page.", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| kind: { type: "string", enum: ["ownership", "officer", "filing", "sanction", "asset", "status"], description: "Optional: filter by event kind." }, | ||
| kind: { type: "string", enum: ["ownership", "filing", "watchlist", "sanction"], description: "Optional: filter by event kind. Registries measured on the newest 100 rows of each stream, 2026-08-11 — they move as loaders run, so read each row's `source_registry` rather than trusting this note. `ownership` = registry-recorded control changes; today 100/100 came from `borme` (Spain's Boletín Oficial del Registro Mercantil), NOT GLEIF as earlier versions of this description claimed. `filing` = UK Companies House accounts (100/100 `companies_house`). `watchlist` = OpenSanctions non-sanctions listings — politically-exposed persons, criminal/wanted entries and procurement debarments, not PEPs alone (100/100 `opensanctions`). `sanction` = a designation landing on a sanctions list, e.g. OFAC SDN (100/100 `opensanctions`)." }, | ||
| limit: { type: "number", minimum: 1, maximum: 100, description: "Max events (default 40)." }, | ||
@@ -386,3 +456,11 @@ since: { type: "string", description: "Optional sync cursor (ISO-8601): pass the next_since from your last response to get only events ingested after it — poll this to monitor what's new." }, | ||
| description: | ||
| "Batch-resolve a list of company names or strong identifiers (scheme:value — lei, siren, gb-coh, uen, nip, sec, ofac, eu, un, uk, krs) to canonical WhiteIntel entity ids in ONE call. Each result carries a confidence: 'exact' (identifier match) or 'name' (top name hit). Use this to enrich a whole list — suppliers, counterparties, a portfolio — without one lookup per row. Then feed the ids into get_dossier / trace_ownership_path / get_sanctions. Up to 25 items anonymously, 100 with WHITEINTEL_API_KEY.", | ||
| // SCHEME LIST — KEEP IT IDENTICAL TO lookup_by_identifier's enum ABOVE AND TO app | ||
| // IDENTIFIER_SCHEMES (intel.server.ts). This string drifted in BOTH directions and an | ||
| // agent has no way to find that out except by getting nothing back: | ||
| // · it advertised `nip`, which LINEAR-5147 removed everywhere else — no Polish NIP is | ||
| // stamped on entities.identifier, so `nip:…` resolved to zero rows, silently. | ||
| // · it omitted `br-cnpj`, which IS supported and reaches our LARGEST source (Brazil RFB, | ||
| // ~78M rows measured 2026-08-16) — the one scheme most worth telling an agent about. | ||
| "Batch-resolve a list of company names or strong identifiers (scheme:value — lei, siren, gb-coh, uen, br-cnpj, sec, ofac, eu, un, uk, krs) to canonical WhiteIntel entity ids in ONE call. Each result carries a confidence: 'exact' (identifier match) or 'name' (top name hit); an unmatched row comes back as `{ match: null, confidence: null }`, so check for it rather than assuming positional success. Use this to enrich a whole list — suppliers, counterparties, a portfolio — without one lookup per row. Then feed the ids into get_dossier / trace_ownership_path / get_sanctions. Up to 25 items anonymously (a 26th returns HTTP 400 with the limit spelled out), 100 with WHITEINTEL_API_KEY. " + | ||
| "TREAT `confidence: 'name'` AS A CANDIDATE, NOT A RESOLUTION. It is the top lexical hit and nothing more — measured 2026-08-11, the query 'Tesco' resolved to a FRENCH company literally named TESCO (fr-siren:454067281), not Tesco PLC, while 'gb-coh:00445790' resolved 'exact' to TESCO PLC. Confirm a 'name' match's jurisdiction and identifier before you attach it to a real counterparty; pass an identifier whenever you hold one.", | ||
| inputSchema: { | ||
@@ -405,3 +483,3 @@ type: "object", | ||
| description: | ||
| "WhiteIntel's price list plus the exact machine flow for buying access. One-off cited dossiers (Standard €39: full UBO chain + financial history · Premium €99: additionally itemised assets), bulk packs (5× / 25× at a discount), subscriptions (Investigator €149/seat·mo, Business €1,900/mo) and the metered API. Returns how_an_agent_buys — buy_dossier opens a Stripe Checkout, a human (or payment-capable agent) pays, claim_dossier mints the access token, and get_dossier with that token returns the unlocked report. Static data, no network call — check it before recommending a purchase.", | ||
| "WhiteIntel's price list plus the exact machine flow for buying access. One-off cited dossiers (Standard €39: full UBO chain + financial history · Premium €99: additionally itemised assets), bulk packs (5× / 25× at a discount), subscriptions (Investigator €149/seat·mo, Business €1,900/mo) and the metered API. Returns how_an_agent_buys — buy_dossier opens a Stripe Checkout, a human (or payment-capable agent) pays, claim_dossier mints the access token, and get_dossier with that token returns the unlocked report. Step 0 of that list covers the case with no human present: get_payment_link returns permanent Stripe links you can hand over instead. Static data, no network call — check it before recommending a purchase.", | ||
| inputSchema: { type: "object", properties: {} }, | ||
@@ -413,3 +491,4 @@ handler: () => PRICING, | ||
| description: | ||
| "Start a one-off dossier purchase via guest Stripe Checkout — no WhiteIntel account needed (Stripe collects an email for delivery). Pick a tier ('standard' €39: full UBO chain + financial history · 'premium' €99: additionally itemised assets — vessels, aircraft, securities, real estate) and optionally a bulk pack ('5' or '25' report credits; standard 5×€159 / 25×€599, premium 5×€399 — no premium 25-pack) plus the entity_id (from search_entities) the report is for. Returns checkout_url + next_steps: open the URL so payment can be completed, then feed the session_id from the post-payment redirect to claim_dossier for the access token. See get_pricing for the full price list.", | ||
| "Start a one-off dossier purchase via guest Stripe Checkout — no WhiteIntel account needed (Stripe collects an email for delivery). Pick a tier ('standard' €39: full UBO chain + financial history · 'premium' €99: additionally itemised assets — vessels, aircraft, securities, real estate) and optionally a bulk pack ('5' or '25' report credits; standard 5×€159 / 25×€599, premium 5×€399 — no premium 25-pack) plus the entity_id (from search_entities) the report is for. Returns checkout_url + next_steps: open the URL so payment can be completed, then feed the session_id from the post-payment redirect to claim_dossier for the access token. See get_pricing for the full price list. " + | ||
| "WRONG TOOL IF NOBODY IS THERE TO PAY: the session it mints is single-use and expires in 24 hours, so putting this URL in a report or a message read tomorrow hands over a dead link. Use get_payment_link for a permanent, reusable one (standard tier only — Premium is available solely through this tool). And do not fetch checkout_url yourself; it is a card form, so it must be handed to a human.", | ||
| inputSchema: { | ||
@@ -421,3 +500,3 @@ type: "object", | ||
| entity_id: { type: "string", maxLength: 80, description: "Optional entity id (from search_entities) the dossier should unlock." }, | ||
| entity_name: { type: "string", maxLength: 200, description: "Optional entity display name (shown in Checkout and the delivery email)." }, | ||
| entity_name: { type: "string", maxLength: 200, description: "Optional entity display name, recorded on the Stripe session as an audit trace only — it is NOT displayed anywhere. Since 2026-08-09 the name shown on the invoice and in the delivery email is read from WhiteIntel's own record for entity_id (caller-supplied text is never rendered in mail we send), and the Checkout page shows the Stripe product name. Safe to omit." }, | ||
| }, | ||
@@ -450,3 +529,22 @@ required: ["tier"], | ||
| }, | ||
| // The one tool the hosted /api/mcp had and this package did not (measured 2026-08-11: | ||
| // hosted tools/list = 21 names, stdio = 20, the diff was exactly this one). It matters | ||
| // more than a parity nit: every one of the 205,497 purchase offers this company has ever | ||
| // served went to a machine caller with no card. buy_dossier hands back a cs_live_ session | ||
| // that dies in 24h and can be spent once — worthless in a report a human opens tomorrow. | ||
| // This returns artefacts a human can still complete next week. | ||
| // | ||
| // /api/public/paylinks measured live before shipping this tool (do not advertise an | ||
| // endpoint you have not called): | ||
| // GET https://whiteintel.dev/api/public/paylinks -> HTTP 200, Cache-Control public, | ||
| // max-age=3600, body { links: [standard:25, standard:5, standard:single], how_to_use: [4] } | ||
| // Note what that body does NOT contain: any premium link. The description says so. | ||
| { | ||
| name: "get_payment_link", | ||
| description: | ||
| "PERMANENT, shareable Stripe payment links for the one-off dossiers — use this INSTEAD of buy_dossier whenever you need something you can HAND TO A HUMAN. buy_dossier mints a Checkout Session that is single-use and expires in 24 hours, so it is useless in a report, a ticket or a message the human reads tomorrow; these links never expire and can be reused. Append ?client_reference_id=<entity uuid from search_entities> to bind the purchase to one company — without it the buyer gets a dossier credit, spendable on any entity later. No API key and no WhiteIntel account needed. MEASURED 2026-08-11: the response carries STANDARD-tier links only — single (€39), 5-pack (€159) and 25-pack (€599). There is no Premium payment link, so for Premium (€99) you must still use buy_dossier and have someone finish Checkout inside 24h. You cannot complete any of these yourself: the page is a card form.", | ||
| inputSchema: { type: "object", properties: {} }, | ||
| handler: () => apiGet(`/api/public/paylinks`), | ||
| }, | ||
| { | ||
| name: "claim_dossier", | ||
@@ -474,3 +572,4 @@ description: | ||
| description: | ||
| "Meaning-based entity search over the corpus (BGE-M3 vector ANN over the resolved dossier cards). Finds companies and people whose profile is semantically closest to a natural-language query — a description, a role, a risk pattern — even when no keyword matches. Complements search_entities (lexical/name). Optional kind (Company/Person/Asset) and jurisdiction (ISO code) filters. Returns entity_id, caption, kind, jurisdiction, risk and a similarity score; feed entity_id into get_dossier / trace_ownership_path. (Coverage grows as the embedding backfill runs; results may be sparse until then.)", | ||
| "Meaning-based entity search over the corpus (BGE-M3 vector ANN over the resolved dossier cards). Finds companies and people whose profile is semantically closest to a natural-language query — a description, a role, a risk pattern — even when no keyword matches. Optional kind (Company/Person/Asset) and jurisdiction (ISO code) filters. Returns entity_id, caption, kind, jurisdiction, risk and a similarity score; feed entity_id into get_dossier / trace_ownership_path. " + | ||
| "TODAY THIS IS EFFECTIVELY A RISK-LIST SEARCH, NOT A CORPUS SEARCH. The response carries its own `coverage` object — read it, it is authoritative and it moves. Measured 2026-08-11: embedded 990,055 of a 47,486,969 universe (ratio 0.0208), and per the endpoint's own note that embedded slice is ~99.6% risk-listed and ~97% natural persons. So a query about an ordinary trading company will return sanctioned people and vessels that merely sound related — verified: 'sanctioned russian aluminium holding' returned RU sanctioned SHIPS as its top hits. An empty or off-target result means 'not embedded yet' far more often than 'not found'. ALWAYS pair this with search_entities, which is lexical and covers the full corpus, before concluding anything about an entity's existence. Latency: 6.4s measured on a cold k=5 call — budget for it.", | ||
| inputSchema: { | ||
@@ -491,3 +590,10 @@ type: "object", | ||
| description: | ||
| "Entities most similar to a given one — the nearest corpus dossier cards ('more like this'), for peer discovery and clustering around a known entity. Pass an entity_id from search_entities. Returns entity_id, caption, kind, jurisdiction, risk and a similarity score. (Coverage grows as the embedding backfill runs.)", | ||
| // The "TEMPORARILY UNAVAILABLE … returns 503 for every entity" sentence that used to sit here | ||
| // was true when the ANN index was dropped, and false by the time anyone read it: the index was | ||
| // rebuilt as IVFFlat and measured healthy (valid, 7.7 GB over 985,708 chunks, 2026-08-10). It | ||
| // shipped on npm for weeks telling every installing agent not to bother calling this tool. | ||
| // The real limit is coverage, not availability, and it is stated the same way here as on the | ||
| // hosted /api/mcp so the two agent surfaces cannot disagree about what works. | ||
| "Entities most similar to a given one — the nearest corpus dossier cards ('more like this'), for peer discovery and clustering around a known entity. Pass an entity_id from search_entities. Returns `{ id, count, hits }`, each hit with entity_id, caption, kind, jurisdiction, risk and a similarity score. " + | ||
| "COVERAGE IS PARTIAL AND SKEWED — it draws on the same embedded slice as semantic_search: 990,055 of a 47,486,969 universe (2.1%), ~99.6% risk-listed and ~97% natural persons, measured 2026-08-11 from the sibling endpoint's own `coverage` payload. An entity outside that slice returns `count: 0` with an empty `hits` array and HTTP 200 — that is 'not embedded', NOT 'no peers exist', and it is the common case for ordinary companies (verified: BARCLAYS BANK PLC returns zero). Never report an empty result as a finding about the entity. Fall back to semantic_search or search_entities.", | ||
| inputSchema: { | ||
@@ -508,3 +614,8 @@ type: "object", | ||
| const server = new Server( | ||
| { name: "whiteintel-mcp-server", version: "0.7.0" }, | ||
| // Keep in lockstep with package.json / package-lock.json / server.json / claude-plugin.json — | ||
| // this is the version the client actually sees over the wire, and it silently drifted 0.7.0 | ||
| // vs 0.7.1. Verify after every bump by piping an initialize request into the delivery command | ||
| // and reading serverInfo.version, which is how 0.7.4 was confirmed: | ||
| // npx -y github:Hei33enberg/WhiteIntel-OS | ||
| { name: "whiteintel-mcp-server", version: "0.7.6" }, | ||
| { capabilities: { tools: {} } }, | ||
@@ -511,0 +622,0 @@ ); |
+11
-4
| { | ||
| "name": "@whiteintel/mcp-server", | ||
| "version": "0.7.0", | ||
| "version": "0.7.6", | ||
| "mcpName": "io.github.Hei33enberg/whiteintel", | ||
| "description": "Model Context Protocol server for WhiteIntel — corporate & offshore ownership intelligence. Look up companies, search entities (companies + people), screen sanctions, and trace ownership chains to the ultimate beneficial owner. Freemium: anonymous free tier, or set WHITEINTEL_API_KEY for your plan.", | ||
@@ -21,3 +22,3 @@ "type": "module", | ||
| "start": "node index.js", | ||
| "test": "node --test test/lib.test.js", | ||
| "test": "node --test test/lib.test.js test/manifests.test.js", | ||
| "smoke": "node test/smoke.mjs" | ||
@@ -40,3 +41,6 @@ }, | ||
| "ai-agents", | ||
| "tool-use" | ||
| "tool-use", | ||
| "semantic-search", | ||
| "agent-payments", | ||
| "sanctions-screening" | ||
| ], | ||
@@ -53,4 +57,7 @@ "mcp": { | ||
| "type": "git", | ||
| "url": "https://github.com/Hei33enberg/whiteintel-mcp-server" | ||
| "url": "git+https://github.com/Hei33enberg/whiteintel-mcp-server.git" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/Hei33enberg/whiteintel-mcp-server/issues" | ||
| }, | ||
| "license": "MIT", | ||
@@ -57,0 +64,0 @@ "author": "whiteintel.dev", |
+169
-48
@@ -0,22 +1,29 @@ | ||
| <div align="center"> | ||
| <img src="https://raw.githubusercontent.com/Hei33enberg/whiteintel-mcp-server/main/logo-mark-512.png" width="132" alt="WhiteIntel" /> | ||
| # @whiteintel/mcp-server | ||
| **They trace names. We trace who really owns them.** | ||
| The corporate-ownership & sanctions intelligence layer for AI agents — built for the agentic era. WhiteIntel turns public-registry and offshore-leak data into MCP-native intelligence primitives — **entity search, semantic discovery, ownership-path traversal, sanctions screening, offshore-exposure detection, and fully cited dossiers** — so any AI agent can investigate a company, trace its ultimate beneficial owner, and flag risk in one conversation. Your agent isn't querying a database — it's conducting an investigation. | ||
| **[Read the Methodology →](https://whiteintel.dev/methodology)** | ||
| [](https://www.npmjs.com/package/@whiteintel/mcp-server) | ||
| [](https://github.com/Hei33enberg/whiteintel-mcp-server/actions/workflows/ci.yml) | ||
| [](LICENSE) | ||
| [](https://modelcontextprotocol.io) | ||
| [](https://modelcontextprotocol.io) | ||
| [](https://whiteintel.dev/developers) | ||
| [](https://whiteintel.dev/coverage) | ||
| [](https://whiteintel.dev/sources) | ||
| [](https://whiteintel.dev) | ||
| **Trace ownership. Expose the network.** A [Model Context Protocol](https://modelcontextprotocol.io) | ||
| server that gives any AI agent (Claude Desktop, Cursor, …) **corporate & offshore | ||
| ownership intelligence** from [WhiteIntel](https://whiteintel.dev): look up companies, | ||
| search entities (companies **and** people), screen sanctions, and trace ownership | ||
| chains to the ultimate beneficial owner. | ||
| </div> | ||
| **Freemium** — works anonymously on the free tier, or set `WHITEINTEL_API_KEY` to | ||
| authenticate as your plan and lift the limits (see [Configuration](#configuration)). | ||
| Forwards to WhiteIntel's public REST API | ||
| (`https://whiteintel.dev/api/public/*`) — [OpenAPI spec](https://whiteintel.dev/api/public/openapi.json). | ||
| --- | ||
| ## Install | ||
| ## What's live today | ||
| No install needed — run via `npx`: | ||
| **One command, any MCP agent:** | ||
@@ -27,6 +34,42 @@ ```bash | ||
| ### Claude Desktop / Cursor | ||
| …starts an MCP server with **21 tools** that give any AI agent — Claude Desktop, Cursor, Cline, Windsurf, or your own runtime — **full corporate-ownership intelligence**: search by name or meaning, trace ownership chains to the UBO, screen sanctions across OFAC/EU/UN/UK, detect offshore layering, pull fully cited dossiers with financials and asset layers, and even purchase deeper intelligence through agent-initiated Stripe checkout. Every claim cited to its source, every edge traced to a registry record. | ||
| Add to your MCP client config: | ||
| | Tool | What it does | Category | | ||
| |---|---|---| | ||
| | `search_entities` | Search the corpus (companies + people) by name → entity ids | 🔍 Discovery | | ||
| | `semantic_search` | Meaning-based search (BGE-M3 vector ANN) — find entities by profile, not keywords | 🔍 Discovery | | ||
| | `find_similar` | "More like this" — nearest entities to a known id, for peer discovery and clustering | 🔍 Discovery | | ||
| | `search_companies` | Free-text company-name search → registration number | 🔍 Discovery | | ||
| | `lookup_company` | UK company by Companies House number → record + ownership graph | 📋 Lookup | | ||
| | `lookup_by_identifier` | Resolve by strong id — LEI, OFAC/EU/UN/UK sanctions id, UEN, SEC CIK, KRS, GB-COH, SIREN, Brazil RFB CNPJ | 📋 Lookup | | ||
| | `get_entity` | Full record for one entity + its direct relationships | 📋 Lookup | | ||
| | `resolve` | Batch-resolve names or `scheme:value` ids → canonical entity ids + confidence | 📋 Lookup | | ||
| | `get_dossier` | Structured, fully-cited dossier: identity, ownership/UBO chain, risk, provenance | 📊 Intelligence | | ||
| | `trace_ownership_path` | Walk ownership upward to the ultimate beneficial owner | 📊 Intelligence | | ||
| | `graph_neighbourhood` | Every edge within N hops of an entity, both directions — hard-capped, says when the view is partial | 🕸️ Graph | | ||
| | `graph_path` | How two entities are connected — **bounded, not exhaustive**: `found: false` is not proof of no link | 🕸️ Graph | | ||
| | `get_sanctions` | Sanctions exposure (OFAC/EU/UN/UK) for entity and its resolved cluster siblings | 🛡️ Risk | | ||
| | `check_offshore_exposure` | Flag sanctioned + secrecy-jurisdiction hops in the ownership chain | 🛡️ Risk | | ||
| | `get_company_details` | UK register detail: address, status, SIC, filings, charges, former names | 📋 Lookup | | ||
| | `get_financials` | Filed UK financials YoY (turnover, profit, net assets, cash, employees) | 📊 Intelligence | | ||
| | `get_pulse` | Live corpus activity feed — recent ownership/control changes, sourced | 📊 Intelligence | | ||
| | `get_pricing` | Full price list + machine-readable purchase flow (static, no network call) | 💳 Commerce | | ||
| | `buy_dossier` | Start a one-off dossier purchase via guest Stripe Checkout → `checkout_url` | 💳 Commerce | | ||
| | `get_payment_link` | Permanent, reusable Stripe payment links — the artefact you hand to a human | 💳 Commerce | | ||
| | `claim_dossier` | Redeem a paid session for a 90-day access token (idempotent) | 💳 Commerce | | ||
| **21 callable tools** — 4 Discovery + 4 Lookup + 4 Intelligence + 2 Graph + 2 Risk + 3 Commerce + 1 Feed + 1 Pricing. All read-only except `buy_dossier` (opens Stripe — money moves only when a human completes it) and `claim_dossier` (redeems an already-paid session). Ids flow between tools: search → get_dossier → trace_ownership_path → get_sanctions. | ||
| ## Quickstart (60 seconds) | ||
| > **Distribution:** the package is on npm — `npx -y @whiteintel/mcp-server` Just Works. | ||
| **1. Run it.** No key needed — works anonymously on the free tier: | ||
| ```bash | ||
| npx -y @whiteintel/mcp-server | ||
| ``` | ||
| **2a. Claude Desktop / Cursor** — add to your MCP config: | ||
| ```json | ||
@@ -44,47 +87,90 @@ { | ||
| The `env` block is optional — omit it to use the anonymous free tier. | ||
| **2b. Claude Code CLI:** | ||
| ## Tools | ||
| ```bash | ||
| claude mcp add whiteintel -- npx -y @whiteintel/mcp-server | ||
| ``` | ||
| | Tool | What it does | | ||
| | --- | --- | | ||
| | `lookup_company` | UK company by Companies House number → record + ownership graph (officers, PSCs, parent/subsidiary edges). | | ||
| | `search_companies` | Free-text company-name search → registration number. | | ||
| | `search_entities` | Search the corpus (companies + people), live + demo investigations → entity ids. | | ||
| | `get_entity` | Full record for one entity + its direct relationships. | | ||
| | `get_dossier` | Structured, fully-cited dossier: cross-source identity, ownership/UBO chain, risk signals, provenance. Optional `token` (from `claim_dossier`) unlocks the paid depth. | | ||
| | `trace_ownership_path` | Walk ownership upward from a root entity to the ultimate beneficial owner. | | ||
| | `lookup_by_identifier` | Resolve an entity by a strong id — LEI, OFAC/EU/UN/UK sanctions id, UEN, NIP, SEC CIK, KRS, GB-COH. | | ||
| | `get_sanctions` | An entity's sanctions exposure (OFAC/EU/UN/UK) for it and its resolved cluster siblings, with sources. | | ||
| | `check_offshore_exposure` | Walk the ownership chain and flag sanctioned + secrecy-jurisdiction hops (offshore-layering lead). | | ||
| | `get_company_details` | UK register detail: registered address, status, type, incorporation date, SIC codes + the filing/compliance layer (accounts, overdue flags, charges, former names). | | ||
| | `get_financials` | Filed UK financials year-over-year (turnover, profit, net assets, cash, employees) from Companies House iXBRL accounts. | | ||
| | `get_pulse` | The live corpus activity feed — recent ownership/control changes, newest first, each sourced; optional `since` cursor to stream only what's new. | | ||
| | `resolve` | Batch-resolve a list of names or `scheme:value` ids → canonical entity ids + confidence, in one call (enrich a whole supplier / portfolio list). | | ||
| | `get_pricing` | The honest price list (one-off dossiers, packs, subscriptions, metered API) + the exact machine flow for buying access. Static, no network call. | | ||
| | `buy_dossier` | Start a one-off dossier purchase via guest Stripe Checkout (Standard €39 / Premium €99, optional 5/25 packs) → returns a `checkout_url`. | | ||
| | `claim_dossier` | Redeem a paid Checkout session (`session_id`) for a 90-day entity-scoped access `token`. Idempotent. | | ||
| **2c. One-click:** add WhiteIntel to your editor at **[whiteintel.dev/developers](https://whiteintel.dev/developers)**. | ||
| All lookup tools are **read-only**; the only side-effectful tools are `buy_dossier` (opens a Stripe Checkout — money moves only when a human completes it) and `claim_dossier` (redeems an already-paid session). Ids flow between tools: `search_entities` / `search_companies` / `resolve` / `lookup_by_identifier` return ids → feed them to `get_dossier` / `trace_ownership_path` / `get_sanctions`. | ||
| The `env` block is optional — omit it to use the anonymous free tier. Set `WHITEINTEL_API_KEY=wi_…` to authenticate as your plan and lift limits. | ||
| ### Try it | ||
| > **You:** "Who ultimately owns Revolut? Check sanctions on the whole chain." | ||
| > | ||
| > **Agent:** calls `search_entities({ query: "Revolut" })` → `trace_ownership_path({ id })` → `get_sanctions({ id })` for each hop → a fully cited ownership chain with sanctions screening at every level. Done. | ||
| > **You:** "Find companies similar to Wirecard and check for offshore exposure." | ||
| > | ||
| > **Agent:** calls `find_similar({ entity_id })` → `check_offshore_exposure({ id })` → flagged secrecy-jurisdiction hops and sanctioned intermediaries across the peer set. | ||
| ## Agents can pay | ||
| An agent can buy the paid depth of a dossier end-to-end, no WhiteIntel account needed: | ||
| An agent can buy the paid depth of a dossier end-to-end, **no WhiteIntel account needed:** | ||
| 1. **`buy_dossier`** `{ tier: "standard" | "premium", entity_id }` → returns a Stripe `checkout_url`. Standard (€39) unlocks the full multi-hop UBO chain + year-over-year financial history; Premium (€99) additionally unlocks itemised assets (vessels, aircraft, securities, real estate). Packs of 5/25 grant reusable report credits. | ||
| 2. A **human (or payment-capable agent) completes payment** at the `checkout_url` — Stripe collects an email and redirects back to whiteintel.dev with `?session_id=cs_…`. | ||
| 3. **`claim_dossier`** `{ session_id }` → `{ token, entity_id, tier }`. Idempotent; returns `402 not_paid` until payment completes. | ||
| 4. **`get_dossier`** `{ id, token }` → the unlocked, fully-cited dossier JSON. Tokens are entity-scoped and valid for 90 days. | ||
| 1. **`buy_dossier`** `{ tier: "standard" | "premium", entity_id }` → returns a Stripe `checkout_url`. Standard (€39) unlocks the full multi-hop UBO chain + financials; Premium (€99) adds aircraft, sanctioned vessels and property; on HIGH-risk or sanctioned subjects it additionally runs a live adverse-media scan (that scan is gated — it does not run on lower-risk entities). | ||
| 2. A **human completes payment** at the `checkout_url` — Stripe collects an email and redirects back. | ||
| 3. **`claim_dossier`** `{ session_id }` → `{ token, entity_id, tier }`. Idempotent; returns `402` until paid. | ||
| 4. **`get_dossier`** `{ id, token }` → the unlocked, fully-cited dossier JSON. Tokens valid 90 days. | ||
| **No human at the keyboard right now?** Step 1 is the wrong tool: a `checkout_url` is single-use and expires in 24 hours, so it is dead by the time someone reads your report. Call **`get_payment_link`** instead — it returns permanent Stripe links you can paste into a document, a ticket or a message, and append `?client_reference_id=<entity uuid>` to bind one to a specific company. Measured 2026-08-11: those links cover the **Standard** tier only (single / 5 / 25); Premium still goes through `buy_dossier`. | ||
| Check **`get_pricing`** first — it returns the full price list plus this flow in machine-readable form. | ||
| ## The corpus | ||
| **~130.7M entities across 31 fused registries** — every claim cited, every edge traced. | ||
| *Measured 2026-08-16 from [whiteintel.dev/api/public/stats](https://whiteintel.dev/api/public/stats) (`entities` = 130,735,728, itself a planner estimate). That endpoint rebuilds its source map by counting registries, so it is always the authority — and a new source shows up there without anyone editing this file.* | ||
| | Source | What | Coverage | | ||
| |---|---|---| | ||
| | **OpenOwnership** | UK PSCs (Persons with Significant Control) | 🇬🇧 Full | | ||
| | **GLEIF** | Global LEI registry + parent/child ownership relations — nightly refresh scheduled | 🌍 Global | | ||
| | **ACRA Singapore** | Singapore company registry | 🇸🇬 Full | | ||
| | **ICIJ Offshore Leaks** | Panama Papers, Paradise Papers, Pandora Papers | 🌍 Offshore | | ||
| | **SEC EDGAR** | US securities filings + beneficial ownership | 🇺🇸 Full | | ||
| | **UK Companies House** | Full UK register — bulk + live filing stream | 🇬🇧 Full | | ||
| | **FAA** | US aircraft registry (tail numbers → owners) | 🇺🇸 Full | | ||
| | **France SIRENE** | French company register | 🇫🇷 Full | | ||
| | **Brazil RFB** | Brazilian federal revenue — CNPJ register | 🇧🇷 Full | | ||
| | **Cyprus DRCOR** | Cypriot register — **officers only** (see scope note below) | 🇨🇾 Loading | | ||
| | **OFAC / EU / UN / UK** | Consolidated sanctions lists | 🌍 Live | | ||
| | **+ 15 more** | registries, sanctions lists & UBO registers | 🌍 Growing | | ||
| ### Cyprus — what it is, and what it is not | ||
| Cyprus went to production on **2026-08-11** and is **still loading** — so we quote no frozen row count here; ask [`/api/public/stats`](https://whiteintel.dev/api/public/stats) for the current figure. | ||
| **Read this before you sell it as Cyprus ownership coverage — it is not.** The Cypriot open data release covers the **nominal layer only: directors, secretaries and trade-name owners.** It contains **no shareholders and no beneficial owners.** Measured on a sample of the loaded edges, roughly **93% are `Directorship`** (Director, Secretary, Authorised Person, general partner) and the remaining **~7% carry the `Ownership` schema with role `Owner`** — those are trade-name proprietorships, a sole trader registered behind a business name, not shareholding in a company. An earlier version of this paragraph said there was "not one ownership edge" in the Cyprus data; that was wrong, and it is corrected here rather than quietly deleted, because a claim about what a source does not contain is exactly the kind of sentence a buyer relies on. | ||
| The practical consequence is unchanged and is the part that matters: a Cypriot **company** will typically answer `trace_ownership_path` and `check_offshore_exposure` with `no_ownership_data`. That verdict means *we hold no ownership edges for this subject*, **not** *this company is cleanly owned*. Do not read the 7% as shareholder coverage — it is not. | ||
| Cypriot records carry a `cy-reg:` identifier. `lookup_by_identifier` does **not** accept that scheme — reach them with `search_entities` using `juris: "cy"`. | ||
| > Contains information from the Cyprus Department of Registrar of Companies and Intellectual Property, licensed under CC BY 4.0. | ||
| **Semantic search** (`semantic_search` / `find_similar`) runs over resolved dossier cards using BGE-M3 embeddings; coverage grows as the embedding backfill completes. Measured 2026-08-11 from the endpoint's own `coverage` payload: **990,055 of a 47,486,969 universe embedded (2.1%), and that slice is ~99.6% risk-listed and ~97% natural persons** — so today these two tools behave much more like a sanctions/PEP search than a corpus search, and an empty result usually means "not embedded yet". Lexical `search_entities` always covers the full corpus; pair it with either of them before drawing a conclusion. | ||
| ## Why WhiteIntel | ||
| > **What's in a name:** White + Intel — **white** as in transparent, open, cited; **intel** as in intelligence, not data. We don't sell raw records — we sell resolution, traversal, and cited delivery. | ||
| Existing corporate-ownership tools were built for compliance analysts clicking web forms. WhiteIntel is the intelligence layer for the agentic era — where the investigator might be a person, an autonomous agent, or an AI workflow, and they all need the same cited, traversed, risk-scored intelligence. | ||
| - **Cited, not claimed.** Every ownership edge, every sanctions flag, every risk signal is traced to a public-registry record with a real effective date. We don't invent or infer — if a source doesn't say it, we don't. | ||
| - **MCP-native, not another API wrapper.** Semantic intelligence primitives — not REST endpoints shoe-horned into tool definitions. One command, any MCP agent. | ||
| - **Freemium by design.** The public corpus is free to explore — no sign-in, no API key, no paywall on search. You pay only for depth: full UBO chains, asset layers, monitoring, and export. | ||
| - **Agents can pay.** The only MCP server where an agent can investigate a company, decide it needs the paid dossier, buy it via Stripe Checkout, and receive the cited intelligence — end-to-end, no human portal needed. | ||
| - **Honest about gaps.** An absent edge means "not yet observed", not "does not exist". Investigative **decision-support**, not a legal determination of beneficial ownership. | ||
| - **No lock-in.** MIT license. Your agent, your data, your investigation. | ||
| ## Data & honesty | ||
| - **Live corpus:** sanctions (OFAC SDN, EU, UN, UK), GLEIF (LEI), ICIJ Offshore | ||
| Leaks, SEC EDGAR, OpenOwnership (UK PSC), plus live UK Companies House lookup — | ||
| cross-source-resolved (a sanctioned party linked to its offshore/registry records). | ||
| - **Demo:** three worked investigations — Meridian (BVI UBO chain), Tideway | ||
| (sanctions exposure), Ardent (VAT-carousel) — flagged `source: "demo"`. | ||
| - **Live corpus:** ~130.7M entities across 31 fused registries (measured 2026-08-16). Live counts, always authoritative over this file: [whiteintel.dev/api/public/stats](https://whiteintel.dev/api/public/stats). | ||
| - **Sources are not uniformly deep.** A registry in the list means we hold *what that registry publishes* — which for some jurisdictions is the officer layer, not ownership. Cyprus is the clearest case (see the scope note above). Never read presence in the source table as ownership coverage. | ||
| - An absent edge means "not yet observed", not "does not exist". | ||
| - Investigative **decision-support**, not a legal determination of beneficial ownership. | ||
| - Semantic search coverage grows as the embedding backfill completes — lexical search always covers the full corpus. | ||
@@ -94,9 +180,44 @@ ## Configuration | ||
| | Env var | Default | Purpose | | ||
| | --- | --- | --- | | ||
| | `WHITEINTEL_API_KEY` | _(none)_ | Optional `wi_` key (whiteintel.dev → Settings → API keys). Forwarded as a Bearer token to authenticate as your plan and lift free-tier limits. | | ||
| |---|---|---| | ||
| | `WHITEINTEL_API_KEY` | _(none)_ | Optional `wi_` key ([whiteintel.dev → Settings → API keys](https://whiteintel.dev/settings)). Authenticates as your plan, lifts free-tier limits. | | ||
| | `WHITEINTEL_API_BASE` | `https://whiteintel.dev` | API origin (SSRF-guarded to whiteintel.dev hosts). | | ||
| | `WHITEINTEL_TIMEOUT_MS` | `30000` | Per-request timeout. | | ||
| ## Ecosystem | ||
| WhiteIntel is part of a growing intelligence platform: | ||
| - **[whiteintel.dev](https://whiteintel.dev)** — the web app: search, graph, dossiers, Pulse feed, monitoring | ||
| - **[WhiteIntel API](https://whiteintel.dev/developers)** — REST API with OpenAPI spec, the same endpoints this MCP server calls | ||
| - **[WhiteIntel Pulse](https://whiteintel.dev/feed)** — live ownership/control change feed across the corporate graph | ||
| - **[@whiteintel/mcp-server](https://www.npmjs.com/package/@whiteintel/mcp-server)** — this package: the MCP intelligence layer | ||
| ## Who's behind this | ||
| WhiteIntel is built and directed by [@Hei33enberg](https://github.com/Hei33enberg) — a self-funded, independent intelligence project. No venture capital, no data brokers, no compromises on citation integrity. | ||
| *Swiss governance · Honest by construction* | ||
| ## Get on the graph | ||
| ```bash | ||
| npx -y @whiteintel/mcp-server # 21 tools, any MCP agent | ||
| ``` | ||
| - **Install** — drop the server into Claude Desktop, Cursor, Cline, Windsurf, or your own runtime (see [Quickstart](#quickstart-60-seconds)). | ||
| - **No key needed** — works on the anonymous free tier out of the box. | ||
| - **Go deeper** — set `WHITEINTEL_API_KEY` for your plan's full depth. | ||
| - **Explore the corpus** — [whiteintel.dev](https://whiteintel.dev) — free to search, no sign-in. | ||
| - **Own it** — [star the repo](https://github.com/Hei33enberg/whiteintel-mcp-server), build on the API, or integrate into your agent pipeline. MIT, no lock-in. | ||
| ## Contributing | ||
| Issues, PRs, and tool ideas welcome. Start with the [CHANGELOG](./CHANGELOG.md) for what's shipped and what's next. If you're building an agent that uses corporate intelligence, we want to hear about it — [intel@whiteintel.dev](mailto:intel@whiteintel.dev). | ||
| **Community:** [GitHub Issues](https://github.com/Hei33enberg/whiteintel-mcp-server/issues) for bugs and features, [GitHub Discussions](https://github.com/Hei33enberg/whiteintel-mcp-server/discussions) for design and help. | ||
| Web: [whiteintel.dev](https://whiteintel.dev) · npm: [@whiteintel/mcp-server](https://www.npmjs.com/package/@whiteintel/mcp-server) · Releases: [GitHub](https://github.com/Hei33enberg/whiteintel-mcp-server/releases) | ||
| ## License | ||
| MIT © whiteintel.dev | ||
| [MIT](./LICENSE) © whiteintel.dev |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
92635
116.73%710
27.24%0
-100%221
121%