fairquery-contact-enricher
OfficialA two-tool MCP server that cleans and dedupes company domain lists for free, then enriches them (paid, $0.003 per successful domain) into one row of public business contact and company data each.
normalize_domains (free, local, no APIFY_TOKEN): reduce URLs, emails or messy strings to bare hostnames (no scheme/path/port/leading www), dedupe keeping first-seen order, list invalid entries and count duplicates — previews exactly what enrich_domains would bill.
enrich_domains (paid, needs APIFY_TOKEN): crawls each domain's homepage plus up to N contact/about/imprint pages via plain HTTP with robots.txt respected and a polite per-site delay; one billable row per enriched domain, errors free.
Returns per domain: company_name, description, emails (role/generic by default, MX + disposable + typo validated), phones in E.164, social profiles (LinkedIn company, X, Facebook, Instagram, YouTube, GitHub), postal address and mail-domain info.
Scope and cost controls: emailScope role/business/all, validateEmails, checkSpfDmarc, defaultPhoneCountry for national-format numbers, maxPagesPerDomain (affects time, not price), 1,000-domain cap with free MAX_DOMAINS_EXCEEDED rows.
Output shape:
countplusitemsof type="domain" rows or free type="error" rows (code, message, retryable).Not for: personal emails of individuals, LinkedIn people search, or JavaScript-only/blocked sites (no browser, no proxy); does no DNS lookup in the free tool.
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., "@fairquery-contact-enricherget contact emails and phones for acme.com and stripe.com"
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.
Website Contact & Company Enricher
Turns company domains into one row each (public business emails, phones, social profiles, company name and address) for $3 per 1,000 domains. You pay $0.003 per domain that returns a result row, and unreachable, blocked or invalid domains are free. Example: enriching 500 domains costs at most 500 × $0.003 = $1.50, plus Apify's $0.00005 Actor-start fee.
Fairquery Apify Actor that turns a list of domains into one clean row per company: business emails (validated for MX, disposable domains and typos), phone numbers (E.164), social profiles, company name, description and postal address, taken from the homepage and its contact / about / imprint pages. Plain HTTP, no browser, robots.txt respected, one request at a time per site.
When to use this Actor (LLM / tool selection)
Use it when an agent or sales-ops workflow needs to:
Add a contact email, phone and LinkedIn company page to a list of company websites (CRM enrichment, lead lists, vendor onboarding)
Find the generic inbox (
info@,sales@,support@,press@) of a company and check that its domain actually receives mailGet the legal company name and address from structured data or the imprint page (EU sites)
Collect a company's official social accounts
Do not use it to find personal emails of individuals or to scrape people's profiles. By default it returns role / generic mailboxes only (emailScope: "role"); personal LinkedIn profiles (/in/…) are never returned.
Related MCP server: Brand Intelligence MCP
How it works
Normalises each input (
https://www.Acme.com/contact,jane@acme.com,acme.com→acme.com) and removes duplicates.Reads
robots.txt(RFC 9309: missing = allowed, 5xx = disallowed) and honoursDisallowrules andCrawl-delayfor user agentFairqueryBot.Fetches the homepage, then the best-scoring same-site pages (contact, Kontakt, contacto, about, imprint/Impressum, legal notice, team…) up to
maxPagesPerDomain, waitingrequestDelayMsbetween requests to the same site.Extracts: JSON-LD
Organization/LocalBusiness(name, legalName, telephone, email, address, sameAs),mailto:andtel:links, Cloudflare-protected emails,name [at] domain [dot] comobfuscation,og:site_name,<address>blocks, social links.Validates emails with the same code as Fairquery Email Validator: syntax, DNS-over-HTTPS MX/null-MX, disposable list, free-provider and typo checks (no SMTP probing).
Input
Field | Type | Default | Description |
|
| (required) | Domains, URLs or emails |
| int |
| 1–15, homepage included |
|
|
|
|
| bool |
| MX / disposable / typo |
| bool |
| SPF/DMARC for the company mail domain |
| string | – | e.g. |
| int |
| Sites in parallel |
| int |
| Delay between requests to one site |
| int |
| Per request |
| int |
| Hard limit per domain (all pages + DNS). At the limit you get the data collected so far ( |
| int |
| Safety cap |
{ "domains": ["apify.com", "stripe.com", "https://basecamp.com/"], "maxPagesPerDomain": 4 }Output
{
"schema_version": "1.0.0", "type": "domain", "input": "apify.com", "domain": "apify.com", "final_url": "https://apify.com/",
"company_name": "Apify Technologies s.r.o.",
"description": "Cloud platform for web scraping, browser automation, AI agents, and data for AI…",
"emails": [
{ "email": "hello@apify.com", "source_url": "https://apify.com/contact", "is_role_account": true, "on_company_domain": true,
"is_free_provider": false, "is_disposable": false, "verdict": "valid", "reasons": ["ROLE_ACCOUNT"], "has_mx": true, "mail_provider": "Google Workspace / Gmail" }
],
"primary_email": "hello@apify.com",
"phones": [{ "e164": "+18889262289", "raw": "+1 888 926 2289", "source_url": "https://stripe.com/contact/sales", "source": "tel" }],
"social_links": { "linkedin": ["https://linkedin.com/company/apify"], "twitter": ["https://x.com/apify"], "github": ["https://github.com/apify"] },
"address": "Na Příkopě 959/27, 11000, Prague, CZ",
"mail_domain": { "has_mx": true, "null_mx": false, "mail_provider": "Google Workspace / Gmail", "has_spf": null, "has_dmarc": null, "dmarc_policy": null },
"pages_crawled": ["https://apify.com/", "https://apify.com/contact"], "pages_blocked_by_robots": 0, "robots_txt": "ok", "email_scope": "role"
}Error rows (type: "error", never charged): INPUT_EMPTY, INVALID_DOMAIN, MAX_DOMAINS_EXCEEDED, SITE_UNREACHABLE, BLOCKED_BY_ROBOTS, ENRICH_FAILED, DOMAIN_TIMEOUT, RUN_TIMEOUT_NEAR.
RUN_SUMMARY (key-value store): pages fetched, pages blocked by robots.txt, emails found vs. excluded by scope, DNS requests, charged events.
Pricing
Event | Price | When |
| $0.003 | Each domain that returns a result row |
| $0.00005 | Once per run, per GB of memory (Apify default event) |
Error rows (SITE_UNREACHABLE, BLOCKED_BY_ROBOTS, INVALID_DOMAIN, ENRICH_FAILED, …) are never charged. A domain is charged once, however many pages are crawled (up to maxPagesPerDomain).
Worked example: 1,000 domains from a Google Maps export, of which 120 are unreachable or blocked: 880 × $0.003 = $2.64, plus $0.00005.
To cap spend, set a maximum cost per run in the run options; the Actor stops cleanly at the limit. maxDomains caps how many domains one run processes. Prices are from the Store pricing tab (October 2026).
Memory / runtime
Default 256 MB, no browser, no proxy. With the default politeness settings a domain takes about 3–8 s (homepage + 4 pages, 1 s apart), and 5 domains run in parallel, so 100 domains finish in roughly 1–3 minutes. Sites that block datacenter IPs or need JavaScript to render contact details will return fewer fields. Memory is capped at 512 MB (more does not help: the Actor is network-bound). Each domain has a hard 40 s time limit (domainTimeoutSecs), so one slow or tarpitting site can't hang the run, and every finished domain is saved and charged immediately. The default run timeout is 15 minutes (about 500–1,000 domains); shortly before the timeout the Actor stops starting new domains and adds an uncharged RUN_TIMEOUT_NEAR row listing how many were skipped, so split very large lists or raise the timeout.
MCP / Apify tools
Call from agents via Apify MCP with Actor fairquery/website-contact-company-enricher:
{ "actor": "fairquery/website-contact-company-enricher", "input": { "domains": ["acme.com"] } }Local stdio MCP server
The mcp/ folder is a stdio MCP server (TypeScript, @modelcontextprotocol/sdk) with three tools, all with annotations and output schemas:
Tool | What it does | Cost |
| Runs this Actor and returns the rows. With | Needs |
| Status and paged rows of a run started with | Free, needs |
| Cleans URLs/emails into unique domains, lists invalid ones, and shows | Free, local, no token |
cd mcp && npm install && npm run build
node scripts/smoke-test.mjs # initialize + tools/list + free tool, no token
APIFY_TOKEN=... node scripts/smoke-test.mjs --call # real call{ "mcpServers": { "fairquery-contact-enricher": { "command": "node", "args": ["/path/to/website-contact-company-enricher/mcp/dist/index.js"], "env": { "APIFY_TOKEN": "<your Apify token>" } } } }Docker: the root Dockerfile builds the MCP server image (docker build -t fairquery-contact-enricher-mcp .); it is also the Dockerfile for Glama. The Actor image is .actor/Dockerfile. glama.json lists fairquery as maintainer.
Works well after
Google Maps Scraper: pass the
websitecolumn intodomainsto add a generic inbox, phone, LinkedIn page and legal name to each business.Google Search Results Scraper or any company list: domains, URLs and email addresses are all accepted and normalised.
Next steps: re-check the emails with the Email Validator just before sending, and screen company names with the Sanctions & Watchlist Screener when you onboard suppliers or customers.
Troubleshooting
Symptom | Cause and fix |
Run is rejected because | The input had no domains. The three sample domains in the Console form are prefill only and are not sent with API calls. |
| The site's robots.txt disallows |
| The site blocks datacenter IPs or renders contact details with JavaScript. The Actor uses plain HTTP with no proxy or browser, so such sites return fewer fields. |
Named people's emails missing | By default |
Run slower than expected | Politeness settings: |
FAQ
Where does the data come from?
Only from pages the company publishes on its own domain: homepage, contact, about, imprint and team pages, plus JSON-LD structured data. No third-party databases.
Does it return personal emails or LinkedIn profiles of people?
By default it returns role and generic mailboxes only, and personal LinkedIn profiles (/in/…) are never returned.
Are emails verified?
They are checked for syntax, MX/null-MX, disposable domains and typos (the same code as the Fairquery Email Validator). There is no SMTP probing, so a mailbox is never confirmed.
How much does it cost?
$0.003 per enriched domain ($3 per 1,000), plus a $0.00005 start fee. Unreachable, blocked and invalid domains are free.
How do I limit spend?
Set a maximum cost per run in the run options and/or lower maxDomains.
Is this GDPR-compliant?
The Actor only reads what businesses publish, but business contact data can still be personal data. You need a lawful basis, must honour opt-outs and must follow anti-spam law. See the disclaimer.
Fairquery family
Other Fairquery tools on the Apify Store. Each one returns structured rows with machine-readable error codes, and error rows are never charged:
Email Validator for Agents (MX, Disposable, Typo): syntax, MX, disposable, role and typo checks without sending mail; $0.79 per 1,000 emails
Website Tech, SEO & llms.txt Check for Agents: tech stack, SEO score, AI-crawler rules and llms.txt per site; $1.50 per 1,000 sites
Sanctions & Watchlist Screener (OFAC, EU, UK, UN): fuzzy name screening against official sanctions lists; $3 per 1,000 names + $0.007 per run
LLM Data Guard: PII & Secret Redactor: redact PII and secrets before text reaches an LLM; $1.50 per 1,000 documents + $0.20 per 1,000 findings
Dependency Vulnerability Check for Agents: packages and lockfiles against OSV.dev; $0.50 per 1,000 packages + $1.50 per 1,000 vulnerabilities
EDGAR Company Facts & Filings for Agents: SEC filings and XBRL facts by ticker, CIK or name; from $1.50 per 1,000 companies
Company Jobs Feed: Greenhouse, Lever & Ashby: live jobs from employers' own ATS boards with age and repost signals; $0.99 per 1,000 jobs + $0.0005 per board
Competitor Change Tracker: Pricing, Terms, Careers & Changelog: watch competitors' public pricing, terms, careers and changelog pages for real changes with plain-English summaries and webhook alerts; $0.00049 per page checked + $0.0049 per change
AI Exposure Scanner: Public Secrets, Files & AI-Crawler Policy: passive, scored scan of your own domains and public GitHub repos for leaked secrets (masked), exposed files, AI-crawler policy, security headers and SPF/DMARC; $0.039 per domain + $0.0039 per repo + $0.0019 per finding
MCP server
@fairquery/agent-trust-mcp: one MCP server for Claude Desktop, Cursor and VS Code (npx -y @fairquery/agent-trust-mcp). It has 8 free tools that need no token, plus paid tools that call the PII, email, sanctions and OSV Actors with your own Apify token.
All Fairquery Actors: https://apify.com/fairquery
Kits & templates
Optional guides from Fairquery for wiring this Actor into agents and automations (the Actor works fine without them):
AI Agent Data Toolkit Quick-Start (PDF): MCP setup for Claude Desktop and Cursor, API calls, recipes, cost estimates.
n8n + Zapier Workflow Template Pack: 10 importable n8n workflows for the four original Fairquery Actors (SEC EDGAR, website/tech, email, OSV); it does not include workflows for this Actor yet, but the patterns adapt easily.
Agent Prompt Pack for Research & Lead Ops: 69 prompts for company research, lead cleaning, SEO and security reviews.
Agent Starter Kit: 10 copy-paste MCP + n8n recipes ($5): 10 copy-paste MCP + n8n recipes, with ready-made Claude/Cursor configs and 3 starter workflows.
Disclaimer
Returns only information that companies publish on their own websites. You are responsible for how you use it: business contact data can still be personal data under GDPR/UK GDPR, so have a lawful basis, honour opt-outs, and follow anti-spam law (CAN-SPAM, ePrivacy/PECR) when you contact anyone. DNS checks cannot prove a mailbox exists.
Links
Apify Store: https://apify.com/fairquery/website-contact-company-enricher
GitHub: https://github.com/fairquery/website-contact-company-enricher
Contact: fairquery@proton.me
License: MIT
Development
Local development, tests and deployment notes are in DEVELOPMENT.md. The Apify Store README is .actor/README.md.
Available Tools
3 toolsenrich_domainsEnrich domains with public business contactsARead-onlyIdempotentInspect
WHAT: For each company domain, crawl the homepage plus up to N contact/about/imprint pages (plain HTTP, robots.txt respected, polite per-site delay) and return one row with company_name, description, business/role emails (MX, disposable and typo validated), phones (E.164), social profiles (LinkedIn company, X, Facebook, Instagram, YouTube, GitHub...), postal address and mail-domain info. WHEN TO USE: enriching a lead/CRM list of company websites with public contact data; finding the official contact email or address of a business. WHEN NOT TO USE: to clean/dedupe a messy list first or preview what will be billed, call normalize_domains (free). Not for finding personal emails of individuals, LinkedIn people search, or JavaScript-only sites that need a browser. COST: paid. Runs Apify Actor fairquery/website-contact-company-enricher with your APIFY_TOKEN; pay-per-event "domain-enriched": $0.003 per domain that returns a result row. Unreachable, blocked or invalid domains are free. 100 domains ~ $0.30. ~2-10 s per domain. RETURNS: one type="domain" row per enriched domain (fields are null or empty when not published on the crawled pages) or free type="error" rows (code, message, retryable); with waitForFinish=false, items is empty and run holds run_id for get_enrichment_results. PARAMETERS: domains: entries are split on commas, semicolons and newlines, then each is reduced to its hostname (scheme, path, port and a leading "www." removed; for an email the part after @ is used; other subdomains are kept, so "shop.acme.com" and "acme.com" are separate) and deduplicated, so you pay once per unique enriched domain; unparseable entries give free INVALID_DOMAIN rows (max 1000 domains per run; extras get a free MAX_DOMAINS_EXCEEDED row). Run normalize_domains first to preview exactly what will be billed (billable_count, estimated_max_cost_usd). waitForFinish=false returns at once with run.run_id and the run keeps going in your Apify account (same price); use it for lists over ~100 domains, which may not finish within this call's 280 s wait, then poll get_enrichment_results. maxPagesPerDomain changes run time and coverage, never the price; 1 = homepage only, 3-5 usually finds the contact/imprint page. emailScope only filters which found addresses are returned: "role" keeps generic mailboxes (info@, sales@, support@...) on any domain, "business" adds named addresses on the company's own domain, "all" also keeps third-party/personal addresses - choose the narrowest scope your lawful basis allows. validateEmails=false skips only the DNS part: disposable/typo checks still run locally, has_mx and mail_domain become null and clean addresses get verdict "unknown" instead of "valid"; checkSpfDmarc adds SPF/DMARC for the company mail domain and has no effect when validateEmails=false. defaultPhoneCountry widens phone extraction to national-format numbers in page text (e.g. "030 1234567" with "DE"); without it only tel: links, JSON-LD and +international numbers are parsed, all returned as E.164. EXAMPLE: {"domains":["stripe.com","https://basecamp.com/about"],"maxPagesPerDomain":3} -> 2 domain rows.
| Name | Required | Description | Default |
|---|---|---|---|
| domains | Yes | Required. 1+ non-empty strings: domains, website URLs or email addresses (the domain part is used); comma/semicolon/newline lists are split; deduplicated; billed once per enriched domain. Example: ["apify.com","https://stripe.com","info@basecamp.com"] | |
| emailScope | No | Optional, one of "role" | "business" | "all", default "role" (GDPR-friendly). "role" = generic mailboxes only (info@, sales@, support@); "business" = also named addresses on the company's own domain; "all" = every address published on the pages. Example: "business" | |
| checkSpfDmarc | No | Optional boolean, default false. true adds SPF/DMARC presence and DMARC policy (none|quarantine|reject) for the company mail domain to mail_domain; requires validateEmails=true. | |
| waitForFinish | No | Optional boolean, default true. true waits up to 280 s and returns the rows; false starts the run and returns immediately with run.run_id (poll get_enrichment_results). Same price either way. Example: false | |
| validateEmails | No | Optional boolean, default true. true runs a DNS-over-HTTPS MX lookup per email domain (no SMTP probing); false skips DNS (disposable/typo checks still run, clean emails get verdict "unknown"). | |
| maxPagesPerDomain | No | Optional integer 1-15, default 5. Pages fetched per domain: homepage plus the best contact/about/imprint pages linked from it; affects time, not price. Example: 3 | |
| defaultPhoneCountry | No | Optional, no default. ISO 3166-1 alpha-2 country code, exactly 2 letters, e.g. "US", "DE". When set, national-format numbers in page text are also parsed; otherwise only tel: links, JSON-LD and +international numbers. |
Output Schema
| Name | Required | Description |
|---|---|---|
| run | No | Only with waitForFinish=false: run_id, status, dataset_id, console_url |
| count | Yes | Number of rows in items |
| items | Yes | Dataset rows: type=domain or type=error |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint/idempotent/openWorld, and the description goes well beyond them: robots.txt respected, per-site politeness delay, 2-10s per domain, a 280s wait cap with the waitForFinish=false escape hatch and get_enrichment_results polling path, plus an explicit price model ($0.003 per enriched domain, free for unreachable/blocked/invalid domains, ~1000-domain cap with free MAX_DOMAINS_EXCEEDED rows). That cost and long-running-operation disclosure is exactly the kind of context an agent cannot get from structured fields.
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 WHAT/WHEN/COST/RETURNS/PARAMETERS/EXAMPLE structure is strongly front-loaded with the decision-relevant content first, and the example anchors the call shape. It is long, and the PARAMETERS block restates several descriptions already present in the schema verbatim, which is the only real redundancy.
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 7-parameter, paid, long-running open-world crawl, this covers everything an agent needs: cost exposure, runtime expectations, async polling behavior, null-vs-error row semantics, and the free preview path via normalize_domains. The presence of an output schema is complemented rather than duplicated.
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%, so the baseline would be 3, but the description adds real semantics: hostname reduction rules (leading www removed, subdomains kept as separate billable domains, email local part discarded), dedup/billing consequences, the unparseable-entry INVALID_DOMAIN behavior, the undocumented 1000-domain run cap, and tuning heuristics ('1 = homepage only, 3-5 usually finds the contact/imprint page'). It also clarifies cross-parameter dependencies (checkSpfDmarc inert when validateEmails=false) and the lawful-basis guidance for emailScope.
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 WHAT block gives a specific verb (crawl homepage + contact/about/imprint pages) and resource (company domains), then enumerates the exact output fields returned. An agent knows precisely what this tool produces and can distinguish it from get_enrichment_results and normalize_domains at a glance.
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 has explicit WHEN TO USE (lead/CRM lists, finding official contact email or address) and WHEN NOT TO USE (call normalize_domains to clean/preview first; not for personal emails, LinkedIn people search, or JS-only sites). The named alternative and the condition that selects it are both stated, leaving nothing to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_enrichment_resultsGet results of a background enrichment run (free)ARead-onlyIdempotentInspect
WHAT: Read the status and result rows of an enrich_domains run started with waitForFinish=false, one page at a time. WHEN TO USE: after enrich_domains returned run.run_id, to poll until finished is true and page through the rows; rows already written are returned while the run is still RUNNING. WHEN NOT TO USE: to start an enrichment (enrich_domains) or to preview cost (normalize_domains). Cannot read runs of other Actors. BEHAVIOR: GET requests to the Apify API only (actor, run, dataset); never starts, aborts or re-bills a run. Typically <1 s. Poll every 10-30 s; a run works on 5 domains at a time, ~2-10 s each. COST: free; needs the APIFY_TOKEN that started the run. Reading charges no Actor event (the run was billed per enriched domain when it ran). RETURNS: run (run_id, status, finished) plus one page of the same rows enrich_domains returns; Errors: APIFY_TOKEN_MISSING, RUN_NOT_FOUND, RUN_NOT_FOR_THIS_ACTOR, APIFY_TOKEN_INVALID, APIFY_RUN_LOOKUP_FAILED / APIFY_DATASET_READ_FAILED (retryable). PARAMETERS: runId is run.run_id from enrich_domains (also in the Apify console URL). status is READY | RUNNING | SUCCEEDED | FAILED | ABORTED | TIMED-OUT; finished=true for the last four, after which total no longer changes. A FAILED or TIMED-OUT run still returns the rows it wrote. offset/limit page the rows in write order; total counts all rows written so far, so keep calling with nextOffset until it is null and finished is true. EXAMPLE: {"runId":"a1B2c3D4e5F6g7H8i"} -> {"run":{"status":"SUCCEEDED","finished":true},"count":1,"total":1,"items":[{"type":"domain","domain":"apify.com",...}]}
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Optional page size, integer 1-1000, default 100. Example: 250 | |
| runId | Yes | Required. Apify run ID (letters and digits) from enrich_domains run.run_id. Example: "a1B2c3D4e5F6g7H8i" | |
| offset | No | Optional integer >= 0, default 0; use nextOffset from the previous page. Example: 100 |
Output Schema
| Name | Required | Description |
|---|---|---|
| run | Yes | run_id, status, finished, dataset_id, started_at, finished_at, console_url |
| count | Yes | Rows in this page |
| items | Yes | Dataset rows: type=domain or type=error |
| total | Yes | Rows written by the run so far |
| nextOffset | Yes | Offset of the next page, or null |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint/idempotentHint/non-destructive, and the description adds substantial context beyond them: GET-only calls, never starts/aborts/re-bills, sub-second typical latency, poll cadence of 10-30 s, processing throughput (5 domains at a time, ~2-10 s each), token/auth requirements, cost model, and the full error-code list including which failures are retryable.
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?
Long but fully front-loaded and labeled (WHAT/WHEN/BEHAVIOR/COST/RETURNS/PARAMETERS/EXAMPLE). Every section carries distinct information for a polling tool, and the one-line JSON example compactly conveys the response shape.
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?
Despite an output schema existing, the description still covers status/finished semantics, that FAILED or TIMED-OUT runs return partial rows, and the error taxonomy. Combined with annotations, schema, and output schema, an agent has everything needed to call and interpret this tool correctly.
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 description coverage is 100%, so the baseline is 3, but the description adds real meaning: runId comes from enrich_domains run.run_id (or the console URL), offset/limit page rows in write order, and the caller should loop with nextOffset until it is null while finished is true. That paging contract is not derivable from the schema alone.
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?
Starts with a precise verb+resource+scope: 'Read the status and result rows of an enrich_domains run started with waitForFinish=false, one page at a time.' It explicitly distinguishes itself from siblings enrich_domains (starting a run) and normalize_domains (cost preview), so an agent can route without opening another schema.
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?
Has dedicated WHEN TO USE and WHEN NOT TO USE sections: use after enrich_domains returns run.run_id to poll until finished and page rows; do not use to start an enrichment or preview cost, and cannot read runs of other Actors. Both the trigger condition and the exclusions are stated explicitly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
normalize_domainsNormalize and dedupe domains (free)ARead-onlyIdempotentInspect
WHAT: Turn URLs, email addresses and messy strings into bare registrable hostnames (lowercase, no scheme/path/www), remove duplicates and list entries that are not valid domains. WHEN TO USE: before enrich_domains, to clean an input list and see exactly which domains would be billed. WHEN NOT TO USE: to get contacts, company names or check that a site is reachable - use enrich_domains. Does no DNS lookup. COST: free; local, no network, no APIFY_TOKEN. RETURNS: domains is exactly the list enrich_domains would bill; billable_count and estimated_max_cost_usd preview that bill; invalid entries would only get free error rows there. PARAMETERS: inputs: each element may hold one value or a newline/comma/semicolon-separated list; pieces are trimmed and empty ones dropped. Each piece is lowercased and reduced to its hostname (scheme, path, query, port and a leading "www." removed; for a bare email the part after @ is used); other subdomains are kept, so "blog.acme.com" and "acme.com" stay separate. A piece is invalid unless the host has a dot and a 2+ letter TLD (punycode xn-- hosts accepted). domains keeps first-seen order and duplicates_removed counts repeats after normalization. This is exactly the normalization enrich_domains applies: billable_count = min(domains.length, 1000 per run) and estimated_max_cost_usd = billable_count x $0.003 + the $0.00005 start fee, the most passing the same inputs to enrich_domains can cost (unreachable domains lower it). An empty array is accepted and returns empty lists. EXAMPLE: {"inputs":["https://www.Stripe.com/pricing","info@stripe.com","not a domain"]} -> {"domains":["stripe.com"],"invalid":["not a domain"],"duplicates_removed":1,"billable_count":1,"estimated_max_cost_usd":0.00305}
| Name | Required | Description | Default |
|---|---|---|---|
| inputs | Yes | Required array (may be empty) of strings: URLs, domains or email addresses; each entry may also be a newline/comma/semicolon-separated list. Example: ["https://www.acme.com/contact","sales@acme.com","acme.com, beta.io"] |
Output Schema
| Name | Required | Description |
|---|---|---|
| domains | Yes | Unique normalized domains, input order |
| invalid | Yes | Entries that could not be parsed as a domain |
| billable_count | Yes | Domains one enrich_domains run would process and could bill (max 1000) |
| duplicates_removed | Yes | Count of duplicate entries dropped |
| estimated_max_cost_usd | Yes | Upper bound in USD for enrich_domains on the same inputs |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive/closed-world, but the description adds substantial context beyond them: free/local, no network, no APIFY_TOKEN, no DNS lookup, and the billing-preview semantics (billable_count, estimated_max_cost_usd formula, invalid entries only producing free error rows). Rich disclosure that goes well past 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?
Well-organized with labeled WHAT/WHEN/COST/RETURNS/PARAMETERS/EXAMPLE sections and the core purpose front-loaded. Slightly verbose: the RETURNS and cost-formula details partly restate the output schema, and the PARAMETERS block is long, but almost every line carries actionable information.
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?
Complete for a complex normalization tool: inputs, normalization rules, validity criteria, ordering, billing preview, an end-to-end EXAMPLE with concrete outputs, and cost model are all covered. An agent has everything needed to call it correctly and reason about downstream cost.
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 already 100%, yet the PARAMETERS section adds real meaning beyond it: newline/comma/semicolon list handling, trimming, lowercasing, hostname reduction, www/scheme/path/port stripping, email @-splitting, subdomain retention, the dot+2-letter-TLD validity rule, punycode acceptance, first-seen ordering, and empty-array acceptance. This far exceeds the schema's single-sentence item description.
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?
States a specific verb+resource ('Turn URLs, email addresses and messy strings into bare registrable hostnames') and immediately distinguishes scope from the sibling enrich_domains. An agent can tell exactly what this does and how it differs from enrichment without opening any schema.
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?
Explicit WHEN TO USE ('before enrich_domains, to clean an input list') and WHEN NOT TO USE ('to get contacts, company names or check that a site is reachable - use enrich_domains'), naming the alternative directly. Nothing is left to inference.
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.
3 tool updates
v1.1.0- Changed
enrich_domains2 fields changed- added
Input schema / properties / waitForFinishAdded value: +{ + "description": "Optional boolean, default true. true waits up to 280 s and returns the rows; false starts the run and returns immediately with run.run_id (poll get_enrichment_results). Same price either way. Example: false", + "type": "boolean" +} - added
Output schema / properties / runAdded value: +{ + "additionalProperties": {}, + "description": "Only with waitForFinish=false: run_id, status, dataset_id, console_url", + "type": "object" +}
- Added
get_enrichment_results - Changed
normalize_domains3 fields changed- added
Output schema / properties / billable_countAdded value: +{ + "description": "Domains one enrich_domains run would process and could bill (max 1000)", + "type": "number" +} - added
Output schema / properties / estimated_max_cost_usdAdded value: +{ + "description": "Upper bound in USD for enrich_domains on the same inputs", + "type": "number" +} - changed
Output schema / requiredPrevious value: -[ - "domains", - "invalid", - "duplicates_removed" -]New value: +[ + "domains", + "invalid", + "duplicates_removed", + "billable_count", + "estimated_max_cost_usd" +]
2 tool updates
- Changed
enrich_domains6 fields changed- changed
Input schema / properties / checkSpfDmarc / descriptionPrevious value: -"Default false. Add SPF/DMARC presence and DMARC policy for the company mail domain."New value: +"Optional boolean, default false. true adds SPF/DMARC presence and DMARC policy (none|quarantine|reject) for the company mail domain to mail_domain; requires validateEmails=true." - changed
Input schema / properties / defaultPhoneCountry / descriptionPrevious value: -"ISO 3166-1 alpha-2 code, e.g. \"US\", \"DE\". When set, national-format numbers in page text are also parsed; otherwise only tel: links, JSON-LD and +international numbers. No default."New value: +"Optional, no default. ISO 3166-1 alpha-2 country code, exactly 2 letters, e.g. \"US\", \"DE\". When set, national-format numbers in page text are also parsed; otherwise only tel: links, JSON-LD and +international numbers." - changed
Input schema / properties / domains / descriptionPrevious value: -"Required. Domains, website URLs or email addresses (the domain part is used); comma/semicolon lists are split. Example: [\"apify.com\",\"https://stripe.com\",\"info@basecamp.com\"]."New value: +"Required. 1+ non-empty strings: domains, website URLs or email addresses (the domain part is used); comma/semicolon/newline lists are split; deduplicated; billed once per enriched domain. Example: [\"apify.com\",\"https://stripe.com\",\"info@basecamp.com\"]" - changed
Input schema / properties / emailScope / descriptionPrevious value: -"\"role\" (default, GDPR-friendly) = generic mailboxes only (info@, sales@, support@); \"business\" = also named addresses on the company's own domain; \"all\" = every address published on the pages."New value: +"Optional, one of \"role\" | \"business\" | \"all\", default \"role\" (GDPR-friendly). \"role\" = generic mailboxes only (info@, sales@, support@); \"business\" = also named addresses on the company's own domain; \"all\" = every address published on the pages. Example: \"business\"" - changed
Input schema / properties / maxPagesPerDomain / descriptionPrevious value: -"Pages fetched per domain, 1-15 (default 5): homepage plus the best contact/about/imprint pages linked from it."New value: +"Optional integer 1-15, default 5. Pages fetched per domain: homepage plus the best contact/about/imprint pages linked from it; affects time, not price. Example: 3" - changed
Input schema / properties / validateEmails / descriptionPrevious value: -"Default true. DNS-over-HTTPS MX lookup plus disposable and typo checks on found emails (no SMTP probing)."New value: +"Optional boolean, default true. true runs a DNS-over-HTTPS MX lookup per email domain (no SMTP probing); false skips DNS (disposable/typo checks still run, clean emails get verdict \"unknown\")."
- Changed
normalize_domains1 field changed- changed
Input schema / properties / inputs / descriptionPrevious value: -"Required. URLs, domains or email addresses; each entry may also be a newline/comma/semicolon-separated list. Example: [\"https://www.acme.com/contact\",\"sales@acme.com\"]."New value: +"Required array (may be empty) of strings: URLs, domains or email addresses; each entry may also be a newline/comma/semicolon-separated list. Example: [\"https://www.acme.com/contact\",\"sales@acme.com\",\"acme.com, beta.io\"]"
2 tool updates
v0.1.0- First observed
enrich_domains - First observed
normalize_domains
TDQS
Scored across 3 tools
Each tool has a clearly distinct role: normalize_domains cleans/previews input for free, enrich_domains performs the paid enrichment, and get_enrichment_results polls async runs. The descriptions explicitly state WHEN NOT TO USE, cross-referencing the other tools, so there is no realistic chance of misselection.
All three names follow a consistent snake_case verb_noun pattern: normalize_domains, enrich_domains, get_enrichment_results. The convention is predictable and readable throughout.
Three tools map exactly onto the natural workflow stages (preview, enrich, retrieve async results), with each tool earning its place and no redundancy. This is well-scoped for a narrow contact-enrichment purpose.
The surface covers the full lifecycle: free normalization/preview, paid enrichment, and async result retrieval including paging. Minor gaps exist (no run cancellation/abort or free-domain validation separate from enrichment), but agents can work around them.
Maintenance
Related MCP Connectors
Domain enrichment: email provider, DMARC/SPF/DKIM, WHOIS age, SSL, hosting and CDN per domain.
Enrich any domain into a full company profile with firmographics and buying signals.
Company and contact enrichment: firmographics from a domain or IP, profiles from a work email.
Domain & brand intelligence: company enrichment, tech stack detection, brand research.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables scraping of emails, phone numbers, and social profile links from website domains. Supports batch processing of up to 20 domains and can find company websites by keyword/company name.2MIT
- AlicenseNot gradedqualityFmaintenanceProvides domain and brand intelligence for AI agents, including company enrichment, tech stack detection, and brand research from free public sources with an on-demand cache.MIT
- AlicenseAqualityBmaintenanceEnables enriching company domains into structured firmographics including employee band, industry, HQ, founded year, revenue estimate, logo, and description, with source provenance and data completeness scoring.162 npm1MIT
- AlicenseAqualityBmaintenanceDomain and company intelligence for AI agents. Enables vetting companies, qualifying leads, and mapping targets from free public data without API keys.62 npm1MIT