Skip to main content
Glama
ctscout

ctscout

by minghsuy

ctscout-mcp-server

MCP server for ctscout.dev — named-entity attribution from Certificate Transparency logs (OV/EV only), with optional multi-signal corroboration on Pro. For mapping legal-entity digital footprints, sibling-domain discovery, and SAN-cohort analysis from LLM-driven workflows.

DV-only infrastructure (Let's Encrypt, ZeroSSL, cloud-native shops) is invisible to ctscout by design. See LIMITATIONS.md for what that means in practice.

Seven tools:

  • ctscout_search_company — find apex domains attributed to an organization by name

  • ctscout_search_company_batch — the same, for up to 10 organization names in one call

  • ctscout_lookup_domain — reverse-lookup the organization attributed to one or more domains

  • ctscout_submit_deep_dive — Pro only: queue an asynchronous multi-signal deep dive (see Deep dives)

  • ctscout_get_job — poll a deep dive and read its result

  • ctscout_lookup_lei — one LEI's record, or the LEIs under a legal name (see Research product)

  • ctscout_vendor_customers — a vendor's customer counts, and with a key the customer enumeration

The first three work over the public ctscout.dev /scan API (the batch tool wraps /scan/batch); the deep-dive pair wraps /jobs; the last two read the research product objects at /lei and /vendors. Free tier requires an API key (no email, no signup). A Pro key gets up to 25 rows, a 12-month window and 3,000 lookups a month on /scan, and can submit deep-dive jobs, which return a confidence_band per attribution with the named signals behind it (DNS brand tokens, RDAP, IP/ASN, homepage metadata, favicon). Visual brand verification (VLM) is not part of v1.

Not a cyber-risk-scoring tool. See LIMITATIONS.md for what ctscout is and isn't, the DV-cert coverage gap, and the corrections path.

Release history: see CHANGELOG.md.


Install

For Claude Code, Claude Desktop, Cursor, or any other MCP client. Two ways to connect: hosted (recommended, no install) or local npm (this package).

1. Get a free API key

Visit ctscout.dev and click "Get a free API key". Solve the Turnstile captcha. Copy the key (you can't recover it later — save it now).

All seven tools are hosted at https://ctscout.dev/mcp, including ctscout_lookup_lei and ctscout_vendor_customers. Nothing to install — point your MCP client at the URL with your API key as the X-API-Key header.

Claude Code (CLI):

claude mcp add \
  --scope user \
  --transport http \
  --header "X-API-Key: YOUR_KEY_HERE" \
  ctscout https://ctscout.dev/mcp

This stores the user-scoped connection in ~/.claude.json. Run claude mcp get ctscout, then open /mcp inside Claude Code to check that ctscout is connected. See the official Claude Code MCP guide.

Cursor — add the ctscout entry to mcpServers in ~/.cursor/mcp.json, preserving any existing servers and other settings:

{
  "mcpServers": {
    "ctscout": {
      "url": "https://ctscout.dev/mcp",
      "headers": { "X-API-Key": "YOUR_KEY_HERE" }
    }
  }
}

Check that ctscout is enabled and connected in Cursor's MCP settings. See Cursor's MCP documentation. Clients that only support legacy SSE can use https://ctscout.dev/sse with the same header.

Claude Desktop — use the local command configuration below. Desktop's remote connectors are a separate setup mechanism; this HTTP JSON is not a claude_desktop_config.json entry. See the official remote connector guide.

2b. Claude Desktop or local stdio clients

Claude Desktop can launch this published package as a local stdio process. It still calls https://ctscout.dev, so your network must allow that origin. Install the current Node.js LTS (Node.js 24 LTS is supported), including npm. The package supports Node.js 20 or newer. Verify both commands in a terminal:

node --version
npm --version

You do not need npm login or to clone this repository to use the public package. In Desktop, open Settings → Developer → Edit Config and add ctscout under mcpServers, preserving existing entries:

{
  "mcpServers": {
    "ctscout": {
      "command": "npx",
      "args": ["-y", "ctscout-mcp-server"],
      "env": { "CTSCOUT_API_KEY": "YOUR_KEY_HERE" }
    }
  }
}

The Desktop config is at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS or %APPDATA%\Claude\claude_desktop_config.json on Windows. Fully quit and restart Desktop, then check that the ctscout tools appear. If Desktop cannot find npx, use its full installed path for command; see the official local-server setup and troubleshooting guide.

For Claude Code's local stdio fallback:

claude mcp add --scope user --env CTSCOUT_API_KEY=YOUR_KEY_HERE \
  ctscout -- npx -y ctscout-mcp-server

The hosted endpoint is the authoritative MCP contract and is the recommended path. The stdio package is a compatibility adapter over the same ctscout.dev API for clients that cannot connect to remote MCP servers yet.

Hosted and stdio expose the same seven public tool names and use the shared implementation, including the names-only ctscout_search_company_batch contract (1–10 organization names, ordered partial-failure results) and the deep-dive pair. The hosted adoption shipped in ctscout-worker#417, with production verification of all seven tools. Hosted connects over HTTP using X-API-Key; stdio runs a local process using CTSCOUT_API_KEY.

Both transports advertise an outputSchema on every tool and return structuredContent on successful calls. Results include snapshot and snapshot_source, except the ctscout_submit_deep_dive receipt: it is a job handle, not a warehouse read, so those fields arrive on ctscout_get_job. On scan and job results, snapshot is the warehouse sync date and snapshot_source is "scan" | "unavailable"; on ctscout_lookup_lei and ctscout_vendor_customers it is the research export's version and the source vocabulary is "product" | "unavailable" (see Research product) — the two are different origins on different cadences, so they do not share a label. A failed call (401, 429, timeout) is an isError result with no structuredContent at all, so do not dereference snapshot on it.

Since X-API-Version 2026-09-05 every /scan and /scan/batch answer carries snapshot, the warehouse sync date it was read from (the sync runs daily), so the scan tools report snapshot_source: "scan" with that date; a deep-dive result carries its own (above). snapshot_source is "unavailable" and snapshot is null only when the API sent none, which it does before a fresh deploy's first sync or when its lookup failed. There is deliberately no client-side fallback (a separately fetched date is not tied to the scan's generation). A client must treat a missing or null snapshot as unknown, never as "current". The hosted-side change was ctscout-worker#343.

The stdio package accepts both MCP protocol eras: current clients can discover the server through the stateless 2026-07-28 server/discover flow, while existing clients keep the 2025 initialize handshake.

3. Use it

Start with one concrete request in your connected client:

Use ctscout to find domains attributed to Cloudflare. Show the organization, returned apex domains and snapshot date, and explain the OV/EV-only coverage.

Approve the tool call if prompted. The client should call ctscout_search_company and return a tool result with attributed domains or an explicit no-match result, plus snapshot information when available. Counts and dates vary with the dataset; an empty result is not proof that an organization has no domains. A 401 means the key needs checking; a 429 means its quota or guard was reached. Neither is a successful lookup.

You can then ask who a domain is attributed to or request a batch of company names. All seven tools listed above remain available through either transport.


Related MCP server: certindex-mcp

Free tier vs Pro tier

The authoritative definition is the product page, https://ctscout.dev/#tiers; this table mirrors it.

Free

Pro

Lookups

10 / day

3,000 / month included

Results

Top 5

Top 25

History window

Last 90 days

Up to 12 months

Deep-dive jobs (async)

—

20 / day

Data

Daily snapshot

Daily snapshot

Customer lists

First 100 rows

Full list

Price

$0

$49 / month — subscribe, the key comes by email within a day

Answer pages, research notes, open data and the LEI endpoints need no key at all. A lookup is one query to the API or the MCP tools; on both tiers the snapshot field names the daily sync date the answer was read from, and a deep-dive result carries confidence_band plus the named signals per attribution. The MCP server uses the same API key for both — your tier is determined by the key. If you hit the free quota, the tool returns a 429 error with an upgrade hint. Terms and privacy: https://ctscout.dev/terms/.

Pro is paid monthly by card through Stripe and the key is sent by hand to the email on the receipt. Need more than 3,000 lookups a month, or the whole dataset? Email pro@ctscout.dev.

What the Pro response looks like

/scan returns the (domain, organization, certs, subdomains) table on both tiers; Pro gets more rows and a longer window. A deep-dive job result replaces it with a richer attribution table you can defend in a meeting:

| Domain | Attributed to | Band | Signals | Evidence |
|---|---|---|---|---|
| `coalition.com` | Coalition Inc | ✅ verified | dns_txt_brand_token, og_site_name_match, rdap_registrant_match, +1 | verified via google-site-verification, atlassian-domain-verification |
| `imposter.com` | Coalition Inc | ⚪ insufficient | dns_txt_brand_token | verified via google-site-verification |

Bands map to confidence intervals (verified ≥ multiple strong independent signals, down to insufficient = no signals or signals disagree). The 🚫VLM-veto tag is reserved for visual brand verification overriding the positive signals; VLM does not run in v1, so it never appears yet. Full structured payload is available via response_format: "json".

Deep dives (Pro, async)

A deep dive is the full multi-signal attribution run executed by a batch worker rather than inside the request. It is asynchronous and Pro only (ctscout-worker#344, contract v1):

  1. ctscout_submit_deep_dive (POST /jobs) takes company_name and/or seed_domain (max 10, validated exactly like /scan) and returns a receipt immediately: {job_id, status: "queued", submitted_at, poll}. Nothing is attributed yet. A free key gets HTTP 403 with the API's upgrade text; the quota is 20 submissions per key per day (HTTP 429 over). Submitting is not idempotent — a retry queues a second job.

  2. ctscout_get_job (GET /jobs/{id}) returns {job_id, kind, status, submitted_at, started_at, finished_at, result?, error?}. status is queued | running | done | failed; result is present only when done, error only when failed. Polling is read-only and debits no quota. Job ids are scoped to the submitting key, so an id another key submitted answers 404 exactly like an unknown id.

Poll with backoff: wait about 30 s before the first poll, then back off toward 5 min between polls. The batch worker picks up queued jobs every few minutes and a deep dive can take several minutes to run.

The result is the deep-dive shape — domains with attributed_to and an enrichment object per domain, plus entity, run_metadata, source, signals_degraded — and three fields the batch worker adds: snapshot (the warehouse date the deep dive read from, present on every deep-dive result because the worker sets it), worker_version and signals_attempted. The markdown output renders the band / signals / evidence table under the job's status lines (a /scan never carries one); structuredContent carries the record with the top-level snapshot / snapshot_source resolved from result.snapshot (null / "unavailable" until the job is done). "Attributed" and "candidate" mean what they mean elsewhere in this package: an attribution is what the evidence names for a domain, a candidate is a name-similarity guess — deep dives report attributions with a confidence band, never bare candidates.

Not in v1: visual brand verification (VLM) — vlm_status stays pending or skipped and never vetoes a band; webhooks, cancellation, retries and result expiry are follow-ups on the Worker side.


Research product (LEI and vendor tools)

ctscout_lookup_lei and ctscout_vendor_customers read a different index from the /scan warehouse: precomputed objects published by the ctscout-research refresh and served by ctscout.dev at /lei and /vendors (ctscout-worker#336). The Worker classifies nothing on these routes — it reads one object and returns it — so both tools are free and debit no quota.

ctscout_lookup_lei

Takes exactly one of lei or name; passing both, or neither, is rejected before any network call.

  • { lei } (GET /lei/{lei}, ISO 17442: 18 uppercase alphanumerics plus 2 check digits) returns the entity record: legal_name and country from GLEIF, isin_count (ISINs mapped to the LEI), apex_count (apex domains attributed to it), the first_seen / last_seen observation window, sample_domains (a hash-chosen sample — not a ranking and not a complete list; apex_count is the total. The size is whatever the research export published: the product contract deliberately does not pin it, so read the array rather than assuming a number, and the markdown says how many it listed) and vendors_confirmed, which is a list of vendor slugs you can pass straight to ctscout_vendor_customers.

  • { name } (GET /lei?name=) returns { query, name_match, leis, lei_count, limit, truncated }. leis is capped at limit (20) while lei_count is the total before the cap, so on a truncated answer the two disagree by design — report lei_count, not leis.length.

name_match is also the discriminator between the two answer shapes: it is present on the by-name answer and absent from the record.

name_match: "none" does not mean the company has no LEI. The name index is keyed by the research normalizer's form of the GLEIF legal name; the two spellings the route tries (the lowercased, trimmed query and its locale-suffix normalization) are not that normalizer, so a real entity can miss on a spelling. Retry with the exact GLEIF legal name, or look it up by LEI. The markdown output says this in place of the miss.

The answer also carries normalizer_mismatch: null when the API's normalizer is the one the index was keyed with, or { index, lookup } naming the two versions when they differ — then the normalized spellings are not the index's keys, and a none may be a spelling miss under this API rather than an absent name. The markdown says so in one line above the result; the JSON carries the field as sent, and the overflow envelope keeps the pair, bounded. An API that predates the check omits it.

ctscout_vendor_customers

Takes a vendor slug and, optionally, enumerate.

  • enumerate: false (the default, GET /vendors/{slug}) returns the free summary: vendor_name, vendor_apex (null when the vendor's brand token matches no label it certifies), the customers split, countries_top, co_use and sample_customers (a hash-chosen sample of the confirmed customers, sized by the export).

  • enumerate: true (GET /vendors/{slug}/customers) returns the enumeration: confirmed and candidates rows of { apex, attributed_to, lei }, plus counts and capped. This route needs an active ctscout.dev API key (any tier); without one the tool returns a clear 401 message that also points back at the keyless summary.

Free enumeration returns up to 100 rows from each list. free_slice records that allowance and truncated says whether the API shortened a list for the key's tier; Pro returns free_slice: null and the full published lists. This is separate from the research export's capped flag and MCP's truncation_note. Both response formats preserve those distinctions, including when a large answer collapses to its metadata envelope.

Candidates and confirmed are two different claims and are never summed. A candidate is an apex the vendor certified a hostname for — fan-out alone, which looks identical to an organization certifying hundreds of its own product sites. Confirmed is the DNS-confirmed subset: a vendor is confirmed when a hostname it certified resolves onto a domain it certifies and the customer's own www does not (or another organization certifies the apex). Because confirmed is a subset of candidates, adding the two double-counts; the markdown keeps them in separate tables and the JSON in separate fields.

Two more fields that read wrong if taken at face value: co_use[].confirmed counts this vendor's confirmed customers that the other vendor also certifies — a candidate there, not a mutual confirmation — and countries_top counts confirmed customers that resolved to an LEI only.

counts and capped describe the object the research build published: capped: true means the build itself kept a subset of the candidates. If this MCP server has to drop rows to stay under its own character limit, it writes a separate truncation_note and leaves counts and capped untouched, so a trimmed list never reads as a complete enumeration. The markdown and the structuredContent are two renderings of one bounded record — same rows, same "N listed of M" headings, one note — so the half you read can never describe a list the other half does not show.

Freshness and the 503

When a response is too large, it collapses through one envelope shared by every product object kind. That envelope knows for each field whether a list is a declared sample (partial against a total the record carries) or published complete, names any list it shortens together with the length the API sent, and never drops the provenance fields below — so a shortened list cannot read as a complete one, and a fallback cannot cost you the per-source provenance.

Both tools carry snapshot (the export's as_of) and snapshot_source ("product" when the API reported the version, "unavailable" when it did not — then snapshot is null and freshness is unknown, never "current"), plus snapshot_dates, the per-source provenance from the manifest (the dated GLEIF, ISIN, ELF and Wikidata snapshots and the PSL bundle each join read). The markdown renders both lines. This is a different clock from the /scan warehouse's daily sync: the research export is republished by its own refresh, so these answers move on that slower cadence.

No API key needed for the free routes

/lei and /vendors/{slug} are unauthenticated on ctscout.dev, so ctscout_lookup_lei and ctscout_vendor_customers without enumerate work with no CTSCOUT_API_KEY set at all — the request carries no X-API-Key header rather than an empty one. The server boots without a key and says on stderr which tools still work; the other five return the usual key error per call, and enumerate: true returns the same guidance it returns for an invalid key, without a network round-trip.

Until the refresh has published its first product, /lei and /vendors answer HTTP 503 with {"detail": "Research product not yet published…"}. Both tools surface that as a plain "not published yet" error naming the API's own detail — not as a server outage, and not as an empty result. A 503 carrying any other body is a different thing entirely (a Worker, proxy or upstream failure) and is reported as a temporary availability failure to retry shortly, never as an unpublished product.


What this is, and isn't

ctscout is a digital entity resolution tool — it maps apex domains to organizations attributed in their Certificate Transparency records, corroborated by DNS / RDAP / IP/ASN / homepage / favicon signals in a Pro deep dive.

It is NOT a cyber-risk quantification platform. It does not score security posture, predict breaches, or produce risk ratings. See LIMITATIONS.md for the full disclaimer, coverage gaps, and corrections path.

Coverage at a glance

ctscout's warehouse is built from OV/EV certificates only — the ones where the issuing CA validated the org's legal identity. DV-only infrastructure (Let's Encrypt, ZeroSSL, ACME-defaulting cloud hosts) is invisible to the warehouse.

The warehouse is strongest on: established US/EU enterprise, government, financial services, traditional infrastructure, defense, education.

The warehouse is weak on: modern cloud-native shops (most domains entirely behind Cloudflare/Vercel/Netlify), pre-launch / stealth-mode startups, anything that defaults to DV certs.

When ctscout_lookup_domain returns 0 results, the apex isn't in the warehouse — not necessarily that nobody owns it. See LIMITATIONS.md for the full coverage discussion; current warehouse size is published live at https://ctscout.dev/stats.


Local development

git clone https://github.com/minghsuy/ctscout-mcp.git
cd ctscout-mcp
npm install
npm run build

# Run the test suite (Vitest, no network)
npm test

# Maintainer-only, non-publishing release preflight
npm run release:check

# Run the server with no key: it boots and warns, and the free /lei and
# /vendors tools work; every other tool errors until a key is configured
node dist/index.js

# With a real key
CTSCOUT_API_KEY=your_key node dist/index.js

# Inspect with the official MCP inspector (browser UI)
npm run inspect

Test the protocol handshake without a real key

echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}},"id":1}' | \
  CTSCOUT_API_KEY=fake node dist/index.js

Should respond with the server's capabilities + tool registration. (Tool calls themselves require a real key.)


How it relates to ctscout.dev

This MCP server is a local stdio compatibility adapter over the public ctscout.dev /scan API. It does no auth-handling magic or caching — it translates MCP tool calls into HTTP requests and formats the response for an LLM consumer. Hosted https://ctscout.dev/mcp is the authoritative MCP surface.

If you're building your own integration in Python or another language, you can hit the same /scan endpoint directly. See ctscout.dev for curl examples.


License

MIT. See LICENSE.

The underlying ctscout service uses domain-scout (also MIT) for cert log analysis.

Release CLI compatibility

Maintainer release scripts support npm10 and npm12; npm10.9.8 and npm12.0.2 are exercised with the exact packed-artifact gate. npm12 requires Node ^22.22.2 || ^24.15.0 || >=26.0.0; a fresh Node24 LTS installation meets that requirement. The published MCP runtime still supports Node20+.

The scripts accept npm10's pack array and npm12's package-keyed object, plus registry scalar strings or singleton arrays. They reject multiple results, unsafe archive filenames, wrong packages/versions and missing or mismatched gitHead values. These compatibility checks do not change release authentication or exact-commit resume requirements. scripts/release.sh --check <version> expects an unpublished, reviewed version; it does not publish, tag, or create a GitHub release.

Available Tools

7 tools
ctscout_get_jobPoll a deep-dive job and read its resultA
Read-onlyIdempotent

Read the state of an asynchronous Pro deep dive submitted with ctscout_submit_deep_dive, via GET /jobs/{id}. Read-only and free to repeat: polling debits no quota.

Polling:

  • status is "queued" | "running" | "done" | "failed". Only "done" carries "result"; "failed" carries a short "error".

  • Back off: about 30 s before the first poll, then longer waits up to 5 min. A deep dive runs on a batch worker that picks up queued jobs every few minutes.

  • Pro only, and job ids are scoped to the submitting key: HTTP 404 means not your job or an unknown id.

Args:

  • job_id (string, required): the id returned by ctscout_submit_deep_dive.

  • response_format ('markdown' | 'json', default 'markdown'): output format.

Returns (on success, structuredContent follows the declared outputSchema; a failed call — 401, 403, 404, timeout — is isError with no structuredContent, so never dereference snapshot on a failed call):

  • In markdown: the job status lines; once done, the deep-dive attribution table (domain, attributed to, confidence band, signals, evidence) under a snapshot line. No /scan output carries this table.

  • In JSON, structured as: { "job_id": string, "kind": "deep_dive", "status": "queued" | "running" | "done" | "failed", "submitted_at": string, "started_at": string | null, "finished_at": string | null, "result": { // only when status is "done"; the deep-dive shape (see below), never returned by /scan "entity": {...}, "domains": [ { "domain": string, "attributed_to": string, "enrichment": {...}, "base": {...} } ], "run_metadata": {...}, "source": "live-enriched" | "cache-only", "signals_degraded": boolean, "snapshot": string, // warehouse date (YYYY-MM-DD) the deep dive read from — present, the batch worker sets it "worker_version": string, "signals_attempted": ... }, "error": string, // only when status is "failed" "snapshot": string | null, // copy of result.snapshot once done; null (unknown) before that "snapshot_source": "scan" | "unavailable" // 'scan' = the API response carried the date }

  • "Attributed" means the organization is what the evidence names for that domain, not an ownership claim. "Candidate" means a semantic name-similarity guess that is NOT an attribution. Deep dives return attributions with a confidence band (verified / likely / possible / insufficient), never bare candidates. When "signals_degraded" is true some signals errored: absence of their evidence is not evidence of absence.

  • Visual brand verification (VLM) is NOT included in v1: vlm_status stays "pending" or "skipped" and never vetoes a band.

Examples:

  • Use when: "Is my deep dive abc123 finished?" -> { job_id: "abc123" }

  • Don't use when: you have no job_id — submit first with ctscout_submit_deep_dive, or use the synchronous tools.

Corrections:

Inputs: job_id (required), response_format. Returns: kind, error, job_id, result, +6 more

ctscout_lookup_domainReverse-lookup organization for one or more domainsA
Read-only

Reverse-lookup ctscout.dev's domain-attribution warehouse: given one or more apex domains, return the organization(s) attributed to each.

Args:

  • domains (string[], required): apex domains to look up. Each between 3 and 253 chars. Max 10 per call. Examples: ["gs.com"], ["coalition.com", "at-bay.com"].

  • response_format ('markdown' | 'json', default 'markdown'): output format.

Returns (on success, structuredContent follows the declared outputSchema — the same one as ctscout_search_company; a failed call is isError with no structuredContent):

  • In markdown: a snapshot line, then a table of (domain, attributed to, cert count, subdomain count). Only domains found in the warehouse appear; a missing domain means no attribution in this snapshot, not a negative finding.

  • In JSON: the same structure as ctscout_search_company, including "snapshot" / "snapshot_source". The 'domains' array contains one entry per attributed (domain, org) pair found. Reverse lookups never return semantic candidates.

Examples:

  • Use when: "Who is gs.com attributed to?" -> { domains: ["gs.com"] }

  • Use when: "Are coalition.com and at-bay.com attributed to the same parent?" -> { domains: ["coalition.com", "at-bay.com"] }

  • Don't use when: You have a company name and want to enumerate its domains — use ctscout_search_company instead.

Coverage caveat:

  • Returns 0 results if domain isn't in the warehouse. Either the domain is not in our index, or no OV/EV certs have been issued for it. DV-only domains (Let's Encrypt etc.) are typically not indexed.

  • When a domain IS in the warehouse but the attributed org is a subsidiary (e.g. an Allianz brand domain), the 'org' field shows the cert-subject organization which may differ from the brand on the homepage.

Auth & limits: same as ctscout_search_company.

Corrections:

Inputs: domains (required), response_format. Returns: total, source, domains, snapshot, +7 more

ctscout_lookup_leiLook up one LEI's record, or the LEIs published under a legal nameA
Read-onlyIdempotent

Read the ctscout research product's entity index: one LEI's record (GET /lei/{lei}), or the LEIs filed under a legal name (GET /lei?name=). Free, and it debits no quota — every answer is a precomputed object published by the ctscout-research refresh, not a live query.

Args (exactly one of lei / name; passing both is rejected before any network call):

  • lei (string, optional): an ISO 17442 LEI — 18 uppercase alphanumerics plus 2 check digits, e.g. '549300NDMY0KJK0ZLW17'.

  • name (string, optional): a legal entity name, 1–200 chars, e.g. 'Cloudflare, Inc.'.

  • response_format ('markdown' | 'json', default 'markdown'): output format.

Returns (on success, structuredContent follows the declared outputSchema; a failed call — 400, 404, 503, timeout — is isError with no structuredContent, so never dereference snapshot on it). One tool, two answer shapes; name_match is the discriminator, present only on the by-name answer:

  • By LEI: { "lei": string, "legal_name": string, // GLEIF's legal name "country": string, // GLEIF's country "isin_count": number, // ISINs mapped to this LEI in GLEIF's ISIN-to-LEI file "apex_count": number, // apex domains attributed to this LEI "first_seen": string, "last_seen": string, // warehouse observation window over those apexes "sample_domains": [string], // hash-chosen sample, whatever size the export published — NOT a ranking, not a complete list "vendors_confirmed": [string] // vendor SLUGS: pass one to ctscout_vendor_customers }

  • By name: { "query": string, "name_match": "exact" | "normalized" | "none", "normalizer_mismatch": null | { "index": string, "lookup": string }, // null: the API's normalizer keyed the index. Set: the two versions differ, so a "none" may be a spelling miss under this API. Absent: an API that predates the check. "leis": [string], // capped at "limit" (20) "lei_count": number, // matches BEFORE the cap — can exceed leis.length "limit": number, "truncated": boolean }

  • Both also carry "as_of" / "product_version" (the export version), "snapshot_dates" (the dated GLEIF / ISIN / ELF / Wikidata snapshot and the PSL bundle each join read), and this server's "snapshot" / "snapshot_source" ("product" when the API reported the version, "unavailable" when it did not — then snapshot is null and freshness is unknown, never "current").

What name_match: "none" means (important):

  • It does NOT mean this company has no LEI. The name index is keyed by the research normalizer's form of the GLEIF legal name; the two spellings the route tries (the lowercased, trimmed query and its locale-suffix normalization) are not the index's normalizer, so a real entity can miss on a spelling.

  • Retry with the exact GLEIF legal name, or look the entity up by LEI. Do not report a "none" as an absent LEI.

Vocabulary: a domain is ATTRIBUTED to an entity — that is what the certificate and DNS evidence names, not an ownership claim. A vendor in vendors_confirmed is CONFIRMED, which has a specific meaning: a vendor is confirmed when a hostname it certified resolves onto a domain it certifies and the customer's own www does not, or another organization certifies the apex.

Examples:

  • Use when: "What does ctscout know about LEI 549300NDMY0KJK0ZLW17?" -> { lei: "549300NDMY0KJK0ZLW17" }

  • Use when: "Which LEIs are filed under 'Cloudflare, Inc.'?" -> { name: "Cloudflare, Inc." }

  • Don't use when: you want the domains attributed to a company by cert subject — that is ctscout_search_company against the warehouse, a different index with different coverage.

Coverage & freshness:

  • The product covers LEIs with at least one attributed apex in the research build, so a 404 means "not in this published version", not "no such LEI". An entity has an LEI at all only where a regulator or a counterparty required one, so an absent LEI is not an absent entity either.

  • The export is republished by the ctscout-research refresh, so these answers move on that cadence — slower than the /scan warehouse, which syncs daily. Read "snapshot" for the version actually answered from.

  • Before the first publish the route answers HTTP 503 and this tool returns a plain "not published yet" error. That is expected, not a fault in the query.

Corrections:

Inputs: lei, name, response_format. Returns: lei, leis, as_of, limit, +18 more

ctscout_search_companySearch ctscout by company nameA
Read-only

Search ctscout.dev's domain-attribution warehouse by organization name. Returns apex domains attributed to that organization based on Certificate Transparency log analysis (OV/EV cert subjects matched to entity names).

Args:

  • company_name (string, required): organization name. Partial matches work — 'Goldman' matches 'Goldman Sachs'. Min 2 chars, max 200.

  • strict_match_org_only (boolean, optional): suppress semantic candidates and return only authoritative warehouse org matches.

  • org_match_field ('verbatim' | 'normalized', optional): raw cert-subject substring matching, or the research normalizer's key of the query against the normalized column (legal forms stripped at the edges, '&'/'and' and hyphen/space unified, a leading 'The' dropped, a generic tail like 'Holdings' kept). Leave unset for automatic verbatim-then-normalized fallback.

  • org_match_mode ('substring' | 'word', optional): use word-boundary matching to reduce noise from short/common names.

  • purpose ('underwriting' | 'corporate_family', optional): choose tight operational-attribution defaults or broader corporate-family defaults. Explicit matching controls override the preset.

  • response_format ('markdown' | 'json', default 'markdown'): output format.

Returns (on success, structuredContent follows the declared outputSchema; an error result — 401, 429, timeout — is isError with no structuredContent, so never dereference snapshot on a failed call):

  • "Attributed" means the organization is what the evidence names for that domain, not an ownership claim. On /scan that evidence is the OV/EV certificate subject on both tiers; multi-signal attribution (DNS, RDAP, IP/ASN, homepage, favicon) exists only in a deep-dive job result (ctscout_submit_deep_dive, Pro). "Candidate" means a semantic name-similarity guess that is NOT an attribution.

  • In markdown: a snapshot line, then a table of (domain, attributed to, cert count, subdomain count). When nothing is attributed but match_type is 'semantic', a table of candidate organizations is rendered instead, labelled as candidates.

  • In JSON, structured as: { "domains": [ // attributed pairs; empty when nothing is attributed { "org": string, // attributed organization: the OV/EV certificate subject (both tiers) "apex_domain": string, // e.g. "gs.com" "cert_count": number, // # of distinct certs observed for this pair "subdomain_count": number, // # of distinct subdomains "first_seen": string | null, // warehouse observation time — NOT the CT log SCT / issuance time "last_seen": string | null // warehouse observation time — NOT the CT log SCT / issuance time } ], "total": number, // total matching rows in warehouse "truncated": boolean, // true if response is capped "upgrade_hint": string, // present when truncated "source": "warehouse", // both tiers read the daily warehouse snapshot "match_type": "exact" | "semantic" | "none", // 'semantic' = domains empty, candidates offered "org_match_strategy": string, // which matching pass produced the answer "empty_reason": string, // present on empty results: why nothing was attributed "candidates": [ // only when match_type is 'semantic'; NOT attributions { "org": string, "similarity": number, "top_apex_domain": string | null } ], "snapshot": string | null, // warehouse/D1 sync date (YYYY-MM-DD) the answer was read from (API version 2026-09-05+); null only when the API could not determine it "snapshot_source": "scan" | "unavailable" // 'scan' = API carried the date; 'unavailable' = it did not (snapshot is null). null means unknown freshness, never "current" }

Examples:

  • Use when: "Find all domains attributed to Cloudflare" -> { company_name: "Cloudflare" }

  • Use when: "Which domains are attributed to Goldman?" -> { company_name: "Goldman Sachs" }

  • Don't use when: You have a specific domain and want to find the organization it's attributed to — use ctscout_lookup_domain instead.

Auth & limits:

  • Requires an API key in CTSCOUT_API_KEY. Get a free key (no email) at https://ctscout.dev.

  • Free tier: 10 successful lookups/day, top 5 results from a daily snapshot. The response's "snapshot" field carries that snapshot's sync date (the API reports it since X-API-Version 2026-09-05); when it is null the API could not determine it — treat freshness as unknown, never as current.

  • Pro tier: 3,000 successful lookups/month included, up to 25 rows, a 12-month window; deep-dive jobs (20/day) for multi-signal attribution. $49/month, subscribed from https://ctscout.dev/#tiers.

Error handling:

  • HTTP 401: API key missing or invalid.

  • HTTP 429: a quota on this key is exhausted, on either tier; the error quotes the API's own detail, which names the cap and when it resets. Tier allowances: https://ctscout.dev/#tiers.

  • "No domains found": try a shorter or different company name (see legal-vs-brand caveat below).

Legal-vs-brand caveat (important):

  • The cert subject (O field) uses LEGAL entity names, not brand names.

  • "Travelers Insurance" → 0 results because the legal name is "The Travelers Companies, Inc."

  • "Hartford Financial" → 0 results; legal names are "Hartford Fire Insurance Company" or "The Hartford Financial Services Group".

  • If a brand-name search returns nothing, retry with variants like "X Companies", "X Group", "X Inc", "X Corporation", or "The X". The empty-result markdown output includes these suggestions automatically when the input looks brand-shaped.

Coverage caveat:

  • Best for established US/EU tech companies with OV/EV certs.

  • Limited coverage on small private companies, cyber MGAs, and entities using only DV (Let's Encrypt) certs.

  • Warehouse size (organizations, org-domain pairs, last sync) is not stated here because it changes daily; read the live figures at https://ctscout.dev/stats before treating a miss as meaningful.

Corrections:

Inputs: company_name (required), purpose, org_match_mode, org_match_field, +2 more. Returns: total, source, domains, snapshot, +7 more

ctscout_search_company_batchSearch ctscout by multiple company names in one callA
Read-only

Look up apex domains for up to 10 organization names in a single call, via ctscout.dev's /scan/batch endpoint. Each name is matched exactly like ctscout_search_company; results come back in input order.

Args:

  • company_names (string[], required): 1–10 organization names. Partial matches work — 'Goldman' matches 'Goldman Sachs'. Each 2–200 chars.

  • response_format ('markdown' | 'json', default 'markdown'): output format.

Returns (on success, structuredContent follows the declared outputSchema; an error result — 401, 429, timeout — is isError with no structuredContent, so never dereference snapshot on a failed call):

  • "Attributed" and "candidate" mean exactly what they mean in ctscout_search_company: what the evidence names (the OV/EV certificate subject on /scan, both tiers) vs a semantic name-similarity guess that is NOT an attribution.

  • In markdown: a snapshot line, then one section per company (heading + the same attributed-domains table as ctscout_search_company; a candidate-organizations table when that name's match_type is 'semantic'), followed by remaining quota. Names that failed render an error line instead of a table.

  • In JSON, the batch envelope: { "results": [ { "query": {...}, "domains": [...], "total": number, "match_type": "exact"|"semantic"|"none", "candidates"?: [...] }, // same per-result fields as ctscout_search_company { "query": {...}, "error": { "code": number, "message": string } } ], "remaining_quota": number | null, // null = no remaining count reported (Pro today); a per-day request guard still applies "snapshot": string | null, // sync date shared by every result in the batch (API version 2026-09-05+); null (unknown freshness) only when the API could not determine it "snapshot_source": "scan" | "unavailable" }

Partial-failure semantics (important):

  • This is a 207-style mixed-result envelope, NOT all-or-nothing: one name can fail (its result carries an "error" object with no "domains") while the rest succeed.

  • Quota debits by the number of names in the batch — every name counts once, even zero-result ones. No free riders.

Examples:

  • Use when: "Look up Cloudflare, Fastly, and Akamai" -> { company_names: ["Cloudflare", "Fastly", "Akamai"] }

  • Don't use when: you have a single name (use ctscout_search_company) or a specific domain (use ctscout_lookup_domain).

Auth & limits:

  • Requires an API key in CTSCOUT_API_KEY, same as ctscout_search_company.

  • Oversized batches (>10 names) are rejected with a validation error before any network call and without a partial quota debit.

  • This MCP batch tool intentionally accepts names only. For matching modifiers such as strict_match_org_only, purpose, or org_match_mode, use individual ctscout_search_company calls or the REST /scan/batch endpoint.

Legal-vs-brand and coverage caveats are identical to ctscout_search_company — brand names may need legal-entity variants ("X Companies", "X Group", "The X"), and coverage is best for established US/EU entities with OV/EV certs.

Corrections:

Inputs: company_names (required), response_format. Returns: results, snapshot, remaining_quota, snapshot_source

ctscout_submit_deep_diveSubmit an async Pro deep dive (multi-signal attribution job)A

Queue an asynchronous Pro deep dive on ctscout.dev: the full multi-signal attribution run (CT warehouse + DNS, RDAP, homepage, IP/ASN corroboration) executed by a batch worker, via POST /jobs. Returns a job receipt immediately, NOT results.

Asynchronous, Pro only:

  • The call returns as soon as the job is queued ({job_id, status: "queued", submitted_at}). Nothing is attributed yet.

  • Poll with ctscout_get_job using the returned job_id. Wait about 30 s before the first poll, then back off toward 5 min between polls; the batch worker picks up queued jobs every few minutes and a deep dive can take several minutes to run.

  • Requires a Pro API key. A free key gets HTTP 403 with the API's upgrade text (Pro is $49/month from https://ctscout.dev/#tiers). Quota: 20 submissions per key per day (HTTP 429 over). Submitting is not idempotent — a retry queues a second job.

Args:

  • company_name (string, optional): organization name, matched exactly as in ctscout_search_company (partial, case-insensitive; 2–200 chars).

  • seed_domain (string[], optional): known apex domains to pivot from, max 10. At least one of company_name / seed_domain is required; both may be given. Validated exactly like /scan.

  • response_format ('markdown' | 'json', default 'markdown'): a receipt with polling guidance, or the raw 202 body.

Returns (on success, structuredContent follows the declared outputSchema; a failed call — 401, 403, 429, timeout — is isError with no structuredContent): { "job_id": string, // opaque; pass to ctscout_get_job "status": "queued", "submitted_at": string, "poll": "/jobs/" // informational }

What the finished result contains (read it with ctscout_get_job):

  • The deep-dive result shape (a /scan never carries it): "domains" of attributed apex domains, each with "attributed_to", an "enrichment" object (confidence_band, weight_total, matched_via, evidence, signal_health, vlm_status, vlm_override) and the underlying discovery evidence under "base"; plus "entity", "run_metadata", "source" and "signals_degraded".

  • Plus "snapshot": the warehouse date (YYYY-MM-DD) the deep dive read from. It is present on every deep-dive result because the batch worker sets it, together with "worker_version" and "signals_attempted"; a /scan answer carries its own snapshot from the API.

  • "Attributed" means the organization is what the evidence names for that domain (certificate subject, corroborated by the enrichment signals), not an ownership claim. "Candidate" means a semantic name-similarity guess that is NOT an attribution; a deep dive reports attributions with a confidence band, never bare candidates.

  • Visual brand verification (VLM) is NOT included in v1: vlm_status stays "pending" or "skipped" and never vetoes a band.

Examples:

  • Use when: "Run a full attribution deep dive on CNA Financial" -> { company_name: "CNA Financial" }

  • Use when: "Deep-dive from these seed domains" -> { seed_domain: ["cna.com", "cnasurety.com"] }

  • Don't use when: you want an answer now — ctscout_search_company / ctscout_lookup_domain are synchronous. Don't resubmit while a job is queued or running; poll it.

Corrections:

Inputs: seed_domain, company_name, response_format. Returns: poll, job_id, status, submitted_at

ctscout_vendor_customersA vendor's customer counts, and — with a key — the customer enumerationA
Read-onlyIdempotent

Read the ctscout research product's vendor objects: the free summary for a vendor slug (GET /vendors/{slug}), or the per-customer enumeration (GET /vendors/{slug}/customers, which needs an API key). Debits no quota either way — both are precomputed objects published by the ctscout-research refresh.

Args:

  • slug (string, required): the vendor slug, one lowercase segment, e.g. 'cloudflare'. The values in a LEI record's vendors_confirmed are exactly these slugs.

  • enumerate (boolean, optional, default false): false = the free summary; true = the per-customer enumeration, which requires an active ctscout.dev API key (any tier) in CTSCOUT_API_KEY. Free returns up to 100 rows from each list; Pro returns the full published lists (which may still be export-capped or shortened for MCP). A missing, invalid or revoked key gets HTTP 401 and this tool explains that the summary is still available with enumerate: false.

  • response_format ('markdown' | 'json', default 'markdown'): output format.

Candidates and confirmed are two different claims and are NEVER summed:

  • Candidate = an apex domain this vendor certified a hostname for. Fan-out alone is not a vendor relationship: an organization certifying hundreds of its own product sites looks identical.

  • Confirmed = the DNS-confirmed subset of the candidates. The definition: a vendor is confirmed when a hostname it certified resolves onto a domain it certifies and the customer's own www does not, or another organization certifies the apex.

  • Confirmed is a SUBSET of candidates, so adding the two double-counts. The markdown keeps them in separate tables and the JSON in separate fields; report them apart.

Returns (on success, structuredContent follows the declared outputSchema; a failed call — 400, 401, 404, 503, timeout — is isError with no structuredContent):

  • enumerate: false (the summary): { "slug": string, "vendor_name": string, "vendor_apex": string | null, // null when the vendor's brand token matches no label it certifies "customers": { "candidates": number, "confirmed": number }, "countries_top": [ { "country": string, "confirmed": number } ], // CONFIRMED customers only "co_use": [ { "slug": string, "confirmed": number } ], // see below "sample_customers": [string] // hash-chosen sample of the CONFIRMED customers, whatever size the export published }

  • enumerate: true (the enumeration): { "slug": string, "confirmed": [ { "apex": string, "attributed_to": string | null, "lei": string | null } ], "candidates": [ same row shape ], "counts": { "candidates": number, "confirmed": number }, // what the research build holds "capped": boolean, // true = the build itself kept a subset of the candidates "truncated": boolean, // true = the API shortened a list for the key tier "free_slice": { "rows": number, "full_list": "pro" } | null, // Free limit per list; null for Pro "truncation_note": string // written by THIS server, only when it dropped rows to fit the character limit; counts and capped still describe the API's answer }

  • Both also carry "as_of" / "product_version", "snapshot_dates", and this server's "snapshot" / "snapshot_source" ("product" | "unavailable"; null snapshot means unknown freshness, never "current").

Reading the fields honestly:

  • co_use counts THIS vendor's confirmed customers that the other vendor also certifies — a candidate there, not a mutual confirmation.

  • countries_top counts confirmed customers that resolved to an LEI; candidates and LEI-less customers are not in it.

  • attributed_to is GLEIF's legal name when the apex resolves to one LEI, the single non-vendor certificate organization otherwise, and null when neither holds. It is an attribution, not an ownership claim.

Examples:

  • Use when: "How many customers does Cloudflare have in the index?" -> { slug: "cloudflare" } (report candidates and confirmed separately)

  • Use when: "List Cloudflare's confirmed customers" -> { slug: "cloudflare", enumerate: true }

  • Don't use when: you have a company and want its vendors — read vendors_confirmed from ctscout_lookup_lei instead.

Coverage & freshness:

  • A 404 means the slug is not in the published version, not that the vendor does not exist. The export is republished by the ctscout-research refresh, so these answers move on that cadence rather than the daily /scan warehouse sync.

  • Before the first publish the routes answer HTTP 503 and this tool returns a plain "not published yet" error. That is expected, not a fault in the query.

Corrections:

Inputs: slug (required), enumerate, response_format. Returns: slug, as_of, capped, co_use, +15 more

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.8/5.0

    Scored across 7 tools

    Disambiguation5/5

    Each tool targets a clearly distinct resource and operation: forward domain search, reverse domain lookup, batch search, LEI records, vendor customer data, and the async submit/poll pair. The descriptions cross-reference each other with explicit 'Don't use when' guidance that resolves the only potentially confusable pairs (search_company vs lookup_domain, single vs batch, submit vs poll).

    Naming Consistency4/5

    Six of seven tools follow the clean `ctscout_<verb>_<noun>` pattern with consistent lowercase verbs and the '_batch' modifier used sensibly. `ctscout_vendor_customers` is the one deviation, using a noun phrase rather than a verb, though it is still readable and unambiguous.

    Tool Count5/5

    Seven tools is well-scoped for a domain-attribution data service covering forward search, reverse lookup, LEI entity records, vendor relationships, and the deep-dive job lifecycle. Each tool earns its place with no redundant entries and no missing surface that would require a bloated count.

    Completeness4/5

    The core workflows are fully covered: forward/reverse lookup, batch search, LEI and vendor research products, and the submit→poll→get-result deep-dive lifecycle. Minor gaps exist — there is no job cancellation or job listing, and vendors are only reachable by slug rather than searchable by name — but agents can work around them without dead ends.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • -
      license
      Not graded
      quality
      Not graded
      maintenance
      Enables ethical security testing and attack surface management through SSL certificate validation, CVE queries, subdomain enumeration, security header analysis, and comprehensive reconnaissance capabilities. Designed for authorized penetration testing workflows with responsible disclosure practices.
      -
    • A
      license
      A
      quality
      C
      maintenance
      Enables LLMs to query Certificate Transparency logs via CertIndex API, allowing searches for TLS certificates, subdomains, and certificate metadata.
      6
      49 PyPI
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables certificate transparency log search via crt.sh, allowing users to query SSL/TLS certificates, enumerate subdomains, and retrieve certificate details with no API key required.
      1
      BSD Zero Clause
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables searching Certificate Transparency logs for TLS certificates issued for a domain and its subdomains, retrieving certificate details like issuer, validity, serial, and SANs, and looking up individual certificates by ID or serial.
      323 npm
      MIT