ctscout
This server queries ctscout.dev's domain-attribution warehouse to map legal entities (from OV/EV certificates) to the apex domains they own, and vice versa.
Search by company name (
ctscout_search_company): Input an organization name (partial matches allowed) and retrieve apex domains attributed to it, along with cert counts, subdomain counts, and timestamps.Reverse-lookup by domain (
ctscout_lookup_domain): Input one or more apex domains (up to 10) and get the organization(s) attributed to each, plus sibling domains owned by the same legal entity.Sibling domain & digital footprint discovery: Surface other apex domains attributed to the same legal entity, enabling lateral discovery of an org's full presence.
Parent/subsidiary relationship checking: Compare multiple domains to determine if they share the same attributed organization (e.g., "Are coalition.com and at-bay.com owned by the same parent?").
Flexible output formats: Human-readable markdown tables or structured JSON.
Free tier (with API key): Up to 10 queries/day, top 5 results from a weekly snapshot.
Pro tier: Unlimited queries, full result sets, and live enriched signals (DNS, RDAP, homepage, IP/ASN, VLM visual verification) with a confidence band per attribution.
Coverage: ~6,000 entities indexed from OV/EV certs (strong on established US/EU organizations); DV-only domains (e.g., Let's Encrypt) are not visible.
Limitation: Searches by legal entity name, not brand — common brand names may return zero results, so try alternate legal-name queries.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ctscoutFind all domains attributed to Cloudflare"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 namectscout_search_company_batch— the same, for up to 10 organization names in one callctscout_lookup_domain— reverse-lookup the organization attributed to one or more domainsctscout_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 resultctscout_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).
2a. Hosted endpoint (recommended — zero install)
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/mcpThis 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 --versionYou 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-serverThe 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: dns-intel
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):
ctscout_submit_deep_dive(POST /jobs) takescompany_nameand/orseed_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.ctscout_get_job(GET /jobs/{id}) returns{job_id, kind, status, submitted_at, started_at, finished_at, result?, error?}.statusisqueued|running|done|failed;resultis present only whendone,erroronly whenfailed. 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_nameandcountryfrom GLEIF,isin_count(ISINs mapped to the LEI),apex_count(apex domains attributed to it), thefirst_seen/last_seenobservation window,sample_domains(a hash-chosen sample — not a ranking and not a complete list;apex_countis 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) andvendors_confirmed, which is a list of vendor slugs you can pass straight toctscout_vendor_customers.{ name }(GET /lei?name=) returns{ query, name_match, leis, lei_count, limit, truncated }.leisis capped atlimit(20) whilelei_countis the total before the cap, so on a truncated answer the two disagree by design — reportlei_count, notleis.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(nullwhen the vendor's brand token matches no label it certifies), thecustomerssplit,countries_top,co_useandsample_customers(a hash-chosen sample of the confirmed customers, sized by the export).enumerate: true(GET /vendors/{slug}/customers) returns the enumeration:confirmedandcandidatesrows of{ apex, attributed_to, lei }, pluscountsandcapped. 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 inspectTest 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.jsShould 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 toolsctscout_get_jobPoll a deep-dive job and read its resultARead-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:
Wrong attribution? Open an issue at https://github.com/minghsuy/ctscout-mcp/issues. Missing entity? Email pro@ctscout.dev. Include the spec the job ran on, the result returned and why it is wrong.
| Name | Required | Description | Default |
|---|---|---|---|
| job_id | Yes | The job_id returned by ctscout_submit_deep_dive. | |
| response_format | No | Output format: 'markdown' for the job status and, once done, the deep-dive attribution table (band, signals, evidence); 'json' for the raw job record. | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| kind | No | 'deep_dive'. |
| error | No | Short 'type: message' reason, present only when status is 'failed'. |
| job_id | Yes | |
| result | No | Present only when status is 'done'. |
| status | Yes | 'queued' | 'running' | 'done' | 'failed'. |
| snapshot | Yes | Warehouse/D1 sync date (YYYY-MM-DD) the answer was read from; the warehouse syncs daily. null when the API could not determine it. |
| started_at | No | |
| finished_at | No | |
| submitted_at | Yes | |
| snapshot_source | Yes | 'scan' = the API response carried the date; 'unavailable' = it did not, snapshot is null and must be treated as unknown, never as current. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Extremely transparent about behavior: status values, polling intervals, error handling, no structuredContent on failure, snapshot_source semantics, attribution vs candidate meaning, signals_degraded implications, and VLM exclusion. Annotations are consistent and the description adds substantial behavioral detail.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Well structured with clear headings (Polling, Returns, Examples, Corrections), but it is quite verbose. The length is justified by the complexity of the async result shape and semantics, so it remains effective despite being detailed.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Provides a complete picture for a complex asynchronous tool: full status lifecycle, output shape in both markdown and JSON, success/failure behavior, snapshot handling, attribution caveats, example usage, and correction channels. Nothing an agent needs to call and interpret this tool correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema already describes both parameters with high coverage, and the description reinforces job_id origin and response_format output differences. The extensive Returns section adds meaningful semantics beyond the schema, though the schema alone is already quite clear.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Clearly states it reads the state of an asynchronous deep-dive job and retrieves its result, with the specific verb 'Read' and the resource 'job'. It also distinguishes itself from submission and synchronous tools by naming them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit use and don't-use guidance: use when you have a job_id, don't use without one, and directs to submit or synchronous alternatives. Also explains polling backoff and when to expect completion.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
ctscout_lookup_domainReverse-lookup organization for one or more domainsARead-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:
Wrong attribution? Open an issue at https://github.com/minghsuy/ctscout-mcp/issues. Missing entity? Email pro@ctscout.dev. Include the domain queried, the result returned and why it is wrong.
| Name | Required | Description | Default |
|---|---|---|---|
| domains | Yes | Apex domains to look up (e.g. ['gs.com', 'goldmansachs.com']). Returns the organization(s) attributed to each domain, plus any sibling domains in the warehouse attributed to the same orgs. Max 10. | |
| response_format | No | Output format: 'markdown' for human-readable summary, 'json' for the raw API response. | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| total | No | Matching pairs in the warehouse before any cap. |
| source | No | 'warehouse' on /scan (both tiers); 'live-enriched' / 'cache-only' on a deep-dive result. |
| domains | Yes | Attributed (domain, organization) pairs. Empty when nothing is attributed. |
| snapshot | Yes | Warehouse/D1 sync date (YYYY-MM-DD) the answer was read from; the warehouse syncs daily. null when the API could not determine it. |
| truncated | No | |
| candidates | No | Present only when match_type is 'semantic'. Candidates are not attributions. |
| match_type | No | 'exact' = domains are warehouse attributions; 'semantic' = domains is empty and candidates holds name-similarity guesses; 'none' = nothing matched. |
| empty_reason | No | |
| upgrade_hint | No | |
| snapshot_source | Yes | 'scan' = the API response carried the date; 'unavailable' = it did not, snapshot is null and must be treated as unknown, never as current. |
| org_match_strategy | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations provide readOnlyHint=true, openWorldHint=true, destructiveHint=false. The description adds substantial context beyond these: the open-world semantics are elaborated ('a missing domain means no attribution in this snapshot, not a negative finding'), it discloses the DV-only certification coverage caveat, explains subsidiary vs. cert-subject org differences, and documents auth/limits plus a corrections channel for wrong attributions. This far exceeds what the annotations convey alone.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but earns its length with clearly delimited sections (Args, Returns, Examples, Coverage caveat, Auth & limits, Corrections). The purpose statement is front-loaded and every section addresses a real agent need. Slight redundancy with the input schema's parameter descriptions and with sibling references could be trimmed, but structure is exemplary.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with two response formats, an output schema, and significant open-world caveats, the description is exceptionally complete: it explains both markdown and json return shapes, references the shared output schema, enumerates coverage caveats (DV-only domains, subsidiary attribution), and points to the corrections process. Nothing an agent needs to call it correctly or interpret results is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% (both parameters described), so baseline is 3. The description adds meaningful value on top: explicit char-length constraints (3-253), max-10-per-call limit, concrete usage examples for both parameters, and clarification of what 'markdown' vs 'json' return. Only minor redundancy with the schema's own parameter descriptions prevents a 5.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb+resource ('Reverse-lookup ctscout.dev's domain-attribution warehouse') and states the exact input (apex domains) and output (attributed organizations). It differentiates from the sibling by explicitly declaring 'Reverse lookups never return semantic candidates' and pointing to ctscout_search_company as the forward direction. Purpose is unambiguous and distinct.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit 'Use when' and 'Don't use when' examples with concrete queries ('Who is gs.com attributed to?') and names the alternative tool (ctscout_search_company) for the opposite use case. The guidance is actionable and leaves no inference to the agent.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
ctscout_lookup_leiLook up one LEI's record, or the LEIs published under a legal nameARead-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:
Wrong attribution? Open an issue at https://github.com/minghsuy/ctscout-mcp/issues. Missing entity? Email pro@ctscout.dev. Include the LEI or name queried, the result returned and why it is wrong.
| Name | Required | Description | Default |
|---|---|---|---|
| lei | No | A single LEI (ISO 17442: 18 uppercase alphanumerics + 2 check digits), e.g. '549300NDMY0KJK0ZLW17'. Give lei or name, not both. | |
| name | No | A legal entity name to look up in the name index, e.g. 'Cloudflare, Inc.'. Give lei or name, not both. | |
| response_format | No | Output format: 'markdown' for the record or the name-match list, 'json' for the raw API response. | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| lei | No | By-LEI answer: the LEI the record is filed under. |
| leis | No | By-name answer: matching LEIs, capped at `limit`. lei_count is the total. |
| as_of | No | The product version this answer was read from. |
| limit | No | By-name answer: the cap applied to `leis`. |
| query | No | By-name answer: the name as submitted. |
| country | No | By-LEI answer: GLEIF's country for the entity. |
| snapshot | Yes | The research product version (YYYY-MM-DD) this answer was read from — the `as_of` of the export the ctscout-research refresh published. null when the API response carried none. |
| last_seen | No | By-LEI answer: latest warehouse observation across this LEI's apexes. |
| lei_count | No | By-name answer: matches before the cap. |
| truncated | No | By-name answer: true when lei_count exceeds limit. |
| apex_count | No | By-LEI answer: apex domains attributed to this LEI in the research build. |
| first_seen | No | By-LEI answer: earliest warehouse observation across this LEI's apexes. |
| isin_count | No | By-LEI answer: ISINs mapped to this LEI in GLEIF's ISIN-to-LEI file. |
| legal_name | No | By-LEI answer: GLEIF's legal name for the entity. |
| name_match | No | By-name answer, and the discriminator between the two shapes: 'exact' | 'normalized' | 'none'. 'none' means neither spelling tried hit the index, NOT that the entity has no LEI. |
| sample_domains | No | By-LEI answer: a hash-chosen sample of the attributed apexes — whatever size the research export published, not a ranking and not a complete list. apex_count is the total; the markdown says so if it lists fewer than the sample carries. |
| snapshot_dates | No | Per-source provenance from the product manifest: elf, gleif, isin, psl, wikidata. Values are the dated snapshot each join read (psl is a bundle identifier, not a date). |
| product_version | No | Same value as as_of; the manifest's version. |
| snapshot_source | Yes | 'product' = the API response carried the export's as_of; 'unavailable' = it did not, snapshot is null and must be treated as unknown, never as current. |
| truncation_note | No | Written by this MCP server, never by the API: present only when a list above was shortened to stay under the response character limit, naming each shortened list and the length the API actually sent. Absent means no list was cut here. |
| vendors_confirmed | No | By-LEI answer: vendor slugs confirmed on this LEI's domains. Pass one to ctscout_vendor_customers. |
| normalizer_mismatch | No | By-name answer: null when the API's normalizer is the one the index was keyed with. Otherwise { index, lookup } names the two versions: the normalized spellings are then not the index's keys, and a 'none' may be a spelling miss under this API rather than an absent name. Absent from an API that predates the check. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description goes well beyond the readOnly/idempotent/openWorld annotations by explaining error semantics ('404 means not in this published version'), the 503 'not published yet' case, the absence of quota debits, and the fact that answers come from a precomputed export rather than a live query. It does not contradict any annotation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but every section serves a distinct purpose: purpose, arguments, return shapes, meaning of 'none', vocabulary, examples, coverage, and corrections. It is front-loaded with the essential purpose and organized with headers and code blocks, so the length is appropriate for the complexity.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
It fully specifies both return shapes, the name_match discriminator, error behavior, snapshot/freshness semantics, and even correction channels. Given the tool's non-obvious 'none' semantics and dual lookup modes, nothing needed to call it correctly appears to be missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
All three parameters are individually described in the schema, and the description adds practical constraints: exactly one of lei/name must be given, passing both is rejected before network access, and the response_format enum is explained with its two possible values.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('Read'), resource ('ctscout research product's entity index'), and scope ('one LEI's record' or 'LEIs filed under a legal name'). It also explicitly differentiates from the sibling tool ctscout_search_company, so an agent can select it without ambiguity.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives explicit use cases ('Use when:') and non-use cases ('Don't use when:') with concrete examples. It also provides guidance on retrying when name_match is 'none' and explains the exact one-of constraint, so when and how to invoke it are clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
ctscout_search_companySearch ctscout by company nameARead-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:
Wrong attribution? Open an issue at https://github.com/minghsuy/ctscout-mcp/issues. Missing entity? Email pro@ctscout.dev. Include the name queried, the result returned and why it is wrong.
| Name | Required | Description | Default |
|---|---|---|---|
| purpose | No | Optional persona preset. 'underwriting' defaults to a tight operational attack-surface set; 'corporate_family' defaults to a broad brand, regional, and family set. Explicitly supplied matching controls always win. | |
| company_name | Yes | Company / organization name to search for. Partial matches work (e.g. 'Goldman' matches 'Goldman Sachs'). Case-insensitive. | |
| org_match_mode | No | Optional, default 'substring'. Use 'word' for short or common-token names to avoid unrelated substring matches. Applies only to verbatim matching. | |
| org_match_field | No | Optional, default 'verbatim'. 'verbatim' substring-matches the raw cert subject. 'normalized' compares the research normalizer's key of the query against the normalized column: GLEIF legal forms stripped at the edges, spelled out or abbreviated ('Aktiengesellschaft' and 'AG', 'Incorporated' and 'Inc.'), diacritics folded, lowercased; '&' and 'and' unify; a hyphen and a space no longer split ('Coca-Cola' and 'Coca Cola'), nor does a leading 'The'; a generic tail like 'Holdings' or 'Group' is part of the name and stays. A query that normalizes to nothing (a bare legal form) matches nothing. Leave unset to try verbatim first and retry normalized after an empty result; set 'normalized' only to skip the verbatim attempt. | |
| response_format | No | Output format: 'markdown' for human-readable summary, 'json' for the raw API response (useful for programmatic processing). | markdown |
| strict_match_org_only | No | Optional, default false. When true, suppress the semantic-name fallback and return only authoritative warehouse organization matches. |
Output Schema
| Name | Required | Description |
|---|---|---|
| total | No | Matching pairs in the warehouse before any cap. |
| source | No | 'warehouse' on /scan (both tiers); 'live-enriched' / 'cache-only' on a deep-dive result. |
| domains | Yes | Attributed (domain, organization) pairs. Empty when nothing is attributed. |
| snapshot | Yes | Warehouse/D1 sync date (YYYY-MM-DD) the answer was read from; the warehouse syncs daily. null when the API could not determine it. |
| truncated | No | |
| candidates | No | Present only when match_type is 'semantic'. Candidates are not attributions. |
| match_type | No | 'exact' = domains are warehouse attributions; 'semantic' = domains is empty and candidates holds name-similarity guesses; 'none' = nothing matched. |
| empty_reason | No | |
| upgrade_hint | No | |
| snapshot_source | Yes | 'scan' = the API response carried the date; 'unavailable' = it did not, snapshot is null and must be treated as unknown, never as current. |
| org_match_strategy | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and destructiveHint=false, but the description goes far beyond by detailing error handling (401/429), truncation via 'truncated' flag, snapshot freshness semantics, match_type behavior, and the difference between attribution and candidates. It discloses that success/failure is conveyed via structuredContent vs isError, and explains auth requirements. No contradictions with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but well-organized with clear section headers (Args, Returns, Examples, Auth, Error handling, Caveats). It front-loads the core purpose and returns structure before diving into caveats. While some repetition exists (snapshot freshness mentioned multiple times), each section serves a distinct purpose. It earns a high score for structure, slightly deducted for length that could be trimmed without losing critical info.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity (6 params, 4 enums, output schema), the description is exceptionally complete. It covers all error scenarios, quota limits, legal-name pitfalls, coverage limitations, and even provides correction channels. The output schema is referenced and the JSON response structure is fully documented. An agent has everything needed to call this tool correctly in various situations.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, but the description adds substantial meaning beyond the schema. For instance, org_match_field gets a full explanation of normalization rules (GLEIF legal forms, '&' vs 'and', leading 'The' dropping, etc.), and org_match_mode is clarified as word-boundary matching. The response_format parameter is expanded with JSON structure details. The description enriches every parameter beyond the schema's basic type/enum descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool searches ctscout.dev's domain-attribution warehouse by organization name and returns attributed apex domains. It explicitly differentiates from sibling tools by naming when NOT to use it (domain lookup) and pointing to ctscout_lookup_domain. The verb 'search' and specific resource 'domain-attribution warehouse' make the purpose unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit when-to-use examples ('Find all domains attributed to Cloudflare') and a direct exclusion for domain-to-org lookups. It also gives detailed usage guidance on legal-vs-brand naming, coverage limitations, and handling empty results with retry suggestions. The 'Don't use when' line names the alternative tool, fulfilling the when-not/alternatives criterion.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
ctscout_search_company_batchSearch ctscout by multiple company names in one callARead-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:
Wrong attribution? Open an issue at https://github.com/minghsuy/ctscout-mcp/issues. Missing entity? Email pro@ctscout.dev. Include the name queried, the result returned and why it is wrong.
| Name | Required | Description | Default |
|---|---|---|---|
| company_names | Yes | Company / organization names to look up in one call (1–10). Each is matched exactly as in ctscout_search_company (partial, case-insensitive). Results come back in input order; individual names can fail independently (partial-failure envelope), so a failed name doesn't sink the batch. | |
| response_format | No | Output format: 'markdown' for a per-company summary, 'json' for the raw batch envelope (useful for programmatic processing). | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| results | Yes | One item per input name, in input order. |
| snapshot | Yes | Warehouse/D1 sync date (YYYY-MM-DD) the answer was read from; the warehouse syncs daily. null when the API could not determine it. |
| remaining_quota | Yes | null = the API reported no remaining count for this key (Pro today); a per-day request guard still applies, and a 429 carries the API's own detail. |
| snapshot_source | Yes | 'scan' = the API response carried the date; 'unavailable' = it did not, snapshot is null and must be treated as unknown, never as current. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnlyHint annotation, the description details partial-failure semantics (207-style envelope), per-name quota debits with no free riders, snapshot freshness behavior, and error handling. It also discloses correction channels and coverage caveats, exceeding annotation transparency.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is lengthy but well-organized with clear sections (Examples, Auth & limits, Partial-failure semantics, Corrections). Every part serves a purpose given the tool's complexity; however, some redundancy exists (e.g., repeated caveats tied to ctscout_search_company), making it slightly less concise than optimal.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description comprehensively covers all necessary context: mixed-result envelope structure, quota accounting, snapshot details, auth, limits, error behavior, distinctions from related tools, and how to report issues. Even with an output schema present, the description fully explains the response shape, exceeding the minimum requirement.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Both parameters are fully described in the schema and further elaborated in the description: company_names includes range, matching behavior, order, and independent failure; response_format explains default and use cases. Schema coverage is 100% and the description adds important context like case-insensitivity and partial matches.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the action ('Look up apex domains'), the resource (up to 10 organization names via /scan/batch), and explicitly distinguishes from siblings: single names go to ctscout_search_company, specific domains to ctscout_lookup_domain. The batch nature is unmistakable.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides concrete when-to-use examples ('Look up Cloudflare, Fastly, and Akamai'), when-not-to-use instructions, authentication requirements (CTSCOUT_API_KEY), and batch size limits. Quota-debit semantics and partial-failure handling are explained, leaving no ambiguity about usage.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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:
Wrong attribution? Open an issue at https://github.com/minghsuy/ctscout-mcp/issues. Missing entity? Email pro@ctscout.dev. Include the job id, the spec submitted and why the result read with ctscout_get_job is wrong.
| Name | Required | Description | Default |
|---|---|---|---|
| seed_domain | No | Known apex domains of the organization to pivot from (e.g. ['gs.com']). Max 10. Give company_name, seed_domain, or both. | |
| company_name | No | Organization name to deep-dive, matched exactly as in ctscout_search_company. Give company_name, seed_domain, or both. | |
| response_format | No | Output format: 'markdown' for a submission receipt with polling guidance, 'json' for the raw 202 body ({job_id, status, submitted_at, poll}). | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| poll | No | Relative API path to poll (informational). |
| job_id | Yes | Opaque id; pass it to ctscout_get_job. |
| status | Yes | 'queued' on submission. |
| submitted_at | Yes | Submission time as reported by the API. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description thoroughly discloses non-idempotency (a retry queues a second job), async behavior, Pro-key requirement, quota limits (HTTP 429), and failure modes (401, 403, 429, timeout), going well beyond the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Although long, the description is well-structured with clear sections (Args, Returns, finished result, Examples, Corrections) and front-loads the core action before diving into details.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers the output schema, the meaning of the finished result, attribution semantics, VLM limitations, polling guidance, and even correction channels, leaving no significant context gaps.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Every parameter is documented with additional detail beyond the schema: company_name matching semantics, seed_domain constraints and validation parity with /scan, and response_format behavior with the raw 202 body.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly identifies the specific action ('Queue an asynchronous Pro deep dive'), the resource ('multi-signal attribution job' on ctscout.dev), and contrasts it with synchronous siblings like ctscout_search_company and ctscout_lookup_domain.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly states when to use the tool, gives concrete examples, warns against using it for immediate answers, names the synchronous alternatives, and advises not to resubmit while a job is queued or running.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
ctscout_vendor_customersA vendor's customer counts, and — with a key — the customer enumerationARead-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:
Wrong attribution? Open an issue at https://github.com/minghsuy/ctscout-mcp/issues. Missing entity? Email pro@ctscout.dev. Include the slug queried, the customer row in question and why it is wrong.
| Name | Required | Description | Default |
|---|---|---|---|
| slug | Yes | The vendor's slug, e.g. 'cloudflare'. The slugs in a LEI record's vendors_confirmed are exactly these values. | |
| enumerate | No | Optional, default false. false returns the free vendor summary (counts, top countries, co-use, a customer sample). true returns the per-customer enumeration from GET /vendors/{slug}/customers, which requires an active ctscout.dev API key (any tier). | |
| response_format | No | Output format: 'markdown' for the counts (or the two customer tables), 'json' for the raw API response. | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| slug | No | The vendor slug the answer is filed under. |
| as_of | No | The product version this answer was read from. |
| capped | No | Enumeration view: true when the research build kept a subset of the candidates. Says nothing about this server's own truncation — see truncation_note. |
| co_use | No | Summary view: other vendors certifying this vendor's confirmed customers. The count is this vendor's confirmed customers that the other vendor also certifies — a candidate there, not a mutual confirmation. |
| counts | No | Enumeration view: the completeness metadata — the rows the research build holds. counts.candidates can exceed candidates.length; see `capped`. |
| snapshot | Yes | The research product version (YYYY-MM-DD) this answer was read from — the `as_of` of the export the ctscout-research refresh published. null when the API response carried none. |
| confirmed | No | Enumeration view: the DNS-confirmed customer rows. |
| customers | No | Summary view: the candidate/confirmed split. Two different claims about the same vendor — confirmed is the DNS-confirmed subset of candidates, so adding them double-counts. |
| truncated | No | Enumeration view: the API shortened either list for this key's tier. |
| candidates | No | Enumeration view: the candidate customer rows. counts.confirmed is a subset of counts.candidates, but when `capped` is true the LISTED candidates are a hash-chosen subset that may omit rows the confirmed list carries. |
| free_slice | No | Enumeration view: free-tier rows allowed per list and the tier for the published list; null for Pro. Separate from export and MCP size caps. |
| vendor_apex | No | Summary view: the vendor's own apex, null when its brand token matches none. |
| vendor_name | No | Summary view: the vendor's certificate name. |
| countries_top | No | Summary view: top countries by CONFIRMED customers (candidates are not counted). |
| snapshot_dates | No | Per-source provenance from the product manifest: elf, gleif, isin, psl, wikidata. Values are the dated snapshot each join read (psl is a bundle identifier, not a date). |
| product_version | No | Same value as as_of; the manifest's version. |
| snapshot_source | Yes | 'product' = the API response carried the export's as_of; 'unavailable' = it did not, snapshot is null and must be treated as unknown, never as current. |
| truncation_note | No | Written by this MCP server, never by the API: present only when rows were dropped from the lists above to stay under the response character limit. `counts` and `capped` still describe the API's answer, not this list. |
| sample_customers | No | Summary view: a hash-chosen sample of the CONFIRMED customers — whatever size the research export published, not a ranking and not a complete list. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
While annotations declare readOnlyHint, openWorldHint, idempotentHint, and destructiveHint, the description adds substantial behavioral context: it debits no quota, explains that a missing/invalid API key yields HTTP 401 and that the summary still works with enumerate: false, clarifies that 404 means the slug isn't in the published version, and that 503 means 'not published yet'. It also distinguishes candidates from confirmed and warns against summing them, and explains the 'capped' and 'truncated' flags. These go far beyond the annotations and are consistent with them.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but well-structured with a clear opening summary, sections for arguments, returns, examples, coverage, and corrections. It is front-loaded with the core purpose. However, it could be tightened; some redundancies exist (e.g., the return structure is described in both prose and the output schema). The length is justified by the tool's complexity, so it earns a 4 rather than lower.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers every aspect an agent needs: the two modes, output structure (including nested fields and their meanings), error handling (400, 401, 404, 503, timeout), freshness semantics (as_of, product_version, snapshot), attribution rules for attributed_to, and the co_use semantics. It even includes examples and a corrections channel. Nothing critical is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Even though the schema provides descriptions for all three parameters, the description enriches each: it ties slug to the exact values in a LEI record's vendors_confirmed, details the free vs Pro limits and key requirements for enumerate, and explains the difference between markdown and json output for response_format. It also adds edge-case behavior like the 401 fallback, which the schema omits.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a precise statement: '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).' It names both modes and explicitly contrasts with a sibling in the 'Don't use when' note, directing to ctscout_lookup_lei for vendor lookups. This makes the tool's purpose unambiguous and distinct.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit use cases with concrete examples: 'How many customers does Cloudflare have in the index?' and 'List Cloudflare's confirmed customers', plus a clear 'Don't use when' instruction that points to an alternative tool. It also explains when to set enumerate: true versus false, leaving no ambiguity about selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
1 tool update
v0.6.1- Changed
ctscout_vendor_customers2 fields changed- added
Output schema / properties / free_sliceAdded value: +{ + "anyOf": [ + { + "additionalProperties": {}, + "properties": { + "full_list": { + "type": "string" + }, + "rows": { + "type": "number" + } + }, + "required": [ + "rows", + "full_list" + ], + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Enumeration view: free-tier rows allowed per list and the tier for the published list; null for Pro. Separate from export and MCP size caps." +} - added
Output schema / properties / truncatedAdded value: +{ + "description": "Enumeration view: the API shortened either list for this key's tier.", + "type": "boolean" +}
7 tool updates
v0.6.0- Added
ctscout_get_job - Changed
ctscout_lookup_domain4 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - changed
Input schema / properties / domains / descriptionPrevious value: -"Apex domains to look up (e.g. ['gs.com', 'goldmansachs.com']). Returns the organization(s) that own each domain, plus any sibling domains in the warehouse owned by the same orgs. Max 10."New value: +"Apex domains to look up (e.g. ['gs.com', 'goldmansachs.com']). Returns the organization(s) attributed to each domain, plus any sibling domains in the warehouse attributed to the same orgs. Max 10." - changed
Input schema / properties / response_format / enumPrevious value: -[ - "json", - "markdown" -]New value: +[ + "markdown", + "json" +] - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {}, + "properties": { + "candidates": { + "description": "Present only when match_type is 'semantic'. Candidates are not attributions.", + "items": { + "additionalProperties": {}, + "properties": { + "org": { + "description": "Candidate organization name — a semantic match, NOT an attribution.", + "type": "string" + }, + "similarity": { + "description": "Name-embedding similarity, 0..1.", + "type": "number" + }, + "top_apex_domain": { + "description": "The apex domain most often attributed to this candidate, if any.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "org" + ], + "type": "object" + }, + "type": "array" + }, + "domains": { + "description": "Attributed (domain, organization) pairs. Empty when nothing is attributed.", + "items": { + "additionalProperties": {}, + "properties": { + "apex_domain": { + "type": "string" + }, + "attributed_to": { + "type": "string" + }, + "cert_count": { + "description": "Distinct certificates observed for this pair.", + "type": "number" + }, + "domain": { + "description": "Deep-dive row shape: the apex domain (no apex_domain field).", + "type": "string" + }, + "first_seen": { + "description": "When the warehouse first ingested this pair (observation time, NOT the CT log SCT / issuance time).", + "type": [ + "string", + "null" + ] + }, + "last_seen": { + "description": "When the warehouse last ingested this pair (observation time, not SCT time).", + "type": [ + "string", + "null" + ] + }, + "org": { + "description": "Organization the domain is attributed to. Free tier: the OV/EV cert subject O field. On /scan, both tiers: the OV/EV certificate subject. Multi-signal attribution lives in a deep-dive job result (attributed_to + enrichment), not here.", + "type": "string" + }, + "subdomain_count": { + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, + "empty_reason": { + "type": "string" + }, + "match_type": { + "description": "'exact' = domains are warehouse attributions; 'semantic' = domains is empty and candidates holds name-similarity guesses; 'none' = nothing matched.", + "type": "string" + }, + "org_match_strategy": { + "type": "string" + }, + "snapshot": { + "description": "Warehouse/D1 sync date (YYYY-MM-DD) the answer was read from; the warehouse syncs daily. null when the API could not determine it.", + "type": [ + "string", + "null" + ] + }, + "snapshot_source": { + "description": "'scan' = the API response carried the date; 'unavailable' = it did not, snapshot is null and must be treated as unknown, never as current.", + "enum": [ + "scan", + "unavailable" + ], + "type": "string" + }, + "source": { + "description": "'warehouse' on /scan (both tiers); 'live-enriched' / 'cache-only' on a deep-dive result.", + "type": "string" + }, + "total": { + "description": "Matching pairs in the warehouse before any cap.", + "type": "number" + }, + "truncated": { + "type": "boolean" + }, + "upgrade_hint": { + "type": "string" + } + }, + "required": [ + "domains", + "snapshot", + "snapshot_source" + ], + "type": "object" +}
- Added
ctscout_lookup_lei - Changed
ctscout_search_company7 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / properties / org_match_fieldAdded value: +{ + "description": "Optional, default 'verbatim'. 'verbatim' substring-matches the raw cert subject. 'normalized' compares the research normalizer's key of the query against the normalized column: GLEIF legal forms stripped at the edges, spelled out or abbreviated ('Aktiengesellschaft' and 'AG', 'Incorporated' and 'Inc.'), diacritics folded, lowercased; '&' and 'and' unify; a hyphen and a space no longer split ('Coca-Cola' and 'Coca Cola'), nor does a leading 'The'; a generic tail like 'Holdings' or 'Group' is part of the name and stays. A query that normalizes to nothing (a bare legal form) matches nothing. Leave unset to try verbatim first and retry normalized after an empty result; set 'normalized' only to skip the verbatim attempt.", + "enum": [ + "verbatim", + "normalized" + ], + "type": "string" +} - added
Input schema / properties / org_match_modeAdded value: +{ + "description": "Optional, default 'substring'. Use 'word' for short or common-token names to avoid unrelated substring matches. Applies only to verbatim matching.", + "enum": [ + "substring", + "word" + ], + "type": "string" +} - added
Input schema / properties / purposeAdded value: +{ + "description": "Optional persona preset. 'underwriting' defaults to a tight operational attack-surface set; 'corporate_family' defaults to a broad brand, regional, and family set. Explicitly supplied matching controls always win.", + "enum": [ + "underwriting", + "corporate_family" + ], + "type": "string" +} - changed
Input schema / properties / response_format / enumPrevious value: -[ - "json", - "markdown" -]New value: +[ + "markdown", + "json" +] - added
Input schema / properties / strict_match_org_onlyAdded value: +{ + "description": "Optional, default false. When true, suppress the semantic-name fallback and return only authoritative warehouse organization matches.", + "type": "boolean" +} - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": {}, + "properties": { + "candidates": { + "description": "Present only when match_type is 'semantic'. Candidates are not attributions.", + "items": { + "additionalProperties": {}, + "properties": { + "org": { + "description": "Candidate organization name — a semantic match, NOT an attribution.", + "type": "string" + }, + "similarity": { + "description": "Name-embedding similarity, 0..1.", + "type": "number" + }, + "top_apex_domain": { + "description": "The apex domain most often attributed to this candidate, if any.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "org" + ], + "type": "object" + }, + "type": "array" + }, + "domains": { + "description": "Attributed (domain, organization) pairs. Empty when nothing is attributed.", + "items": { + "additionalProperties": {}, + "properties": { + "apex_domain": { + "type": "string" + }, + "attributed_to": { + "type": "string" + }, + "cert_count": { + "description": "Distinct certificates observed for this pair.", + "type": "number" + }, + "domain": { + "description": "Deep-dive row shape: the apex domain (no apex_domain field).", + "type": "string" + }, + "first_seen": { + "description": "When the warehouse first ingested this pair (observation time, NOT the CT log SCT / issuance time).", + "type": [ + "string", + "null" + ] + }, + "last_seen": { + "description": "When the warehouse last ingested this pair (observation time, not SCT time).", + "type": [ + "string", + "null" + ] + }, + "org": { + "description": "Organization the domain is attributed to. Free tier: the OV/EV cert subject O field. On /scan, both tiers: the OV/EV certificate subject. Multi-signal attribution lives in a deep-dive job result (attributed_to + enrichment), not here.", + "type": "string" + }, + "subdomain_count": { + "type": "number" + } + }, + "type": "object" + }, + "type": "array" + }, + "empty_reason": { + "type": "string" + }, + "match_type": { + "description": "'exact' = domains are warehouse attributions; 'semantic' = domains is empty and candidates holds name-similarity guesses; 'none' = nothing matched.", + "type": "string" + }, + "org_match_strategy": { + "type": "string" + }, + "snapshot": { + "description": "Warehouse/D1 sync date (YYYY-MM-DD) the answer was read from; the warehouse syncs daily. null when the API could not determine it.", + "type": [ + "string", + "null" + ] + }, + "snapshot_source": { + "description": "'scan' = the API response carried the date; 'unavailable' = it did not, snapshot is null and must be treated as unknown, never as current.", + "enum": [ + "scan", + "unavailable" + ], + "type": "string" + }, + "source": { + "description": "'warehouse' on /scan (both tiers); 'live-enriched' / 'cache-only' on a deep-dive result.", + "type": "string" + }, + "total": { + "description": "Matching pairs in the warehouse before any cap.", + "type": "number" + }, + "truncated": { + "type": "boolean" + }, + "upgrade_hint": { + "type": "string" + } + }, + "required": [ + "domains", + "snapshot", + "snapshot_source" + ], + "type": "object" +}
- Added
ctscout_search_company_batch - Added
ctscout_submit_deep_dive - Added
ctscout_vendor_customers
2 tool updates
v0.2.5- First observed
ctscout_lookup_domain - First observed
ctscout_search_company
TDQS
Scored across 7 tools
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).
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.
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.
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
Related MCP Connectors
Machine-readable entity discovery with provenance, trust and verified source evidence.
Certificate Transparency search: subdomains, certificate history and hostname keyword search.
Public data intelligence for AI agents — CVE, compliance, patents, contracts, domains.
AI-powered threat intelligence, smart contract auditing, and cybersecurity OSINT.
Related MCP Servers
- -licenseNot gradedqualityNot gradedmaintenanceEnables 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.-
- FlicenseNot gradedqualityDmaintenanceEnables domain intelligence lookups including RDAP registration data, DNS records, and Certificate Transparency searches.1-
- AlicenseAqualityBmaintenanceEnables LLMs to query Certificate Transparency logs via CertIndex API, allowing searches for TLS certificates, subdomains, and certificate metadata.6MIT
- AlicenseNot gradedqualityBmaintenanceDomain and company intelligence for AI agents. Enables vetting companies, qualifying leads, and mapping targets from free public data without API keys.MIT