Server definition
- Hash
- sha256:87cbd089406d891b6508b411ed06ea14d2d9c7ba2893ca970a32ad130b905df8
- What it is
- What a remote MCP server returned when asked what it offers: 11 tools
The blob, as servednamed by its sha256
{
"instructions": null,
"tools": [
{
"description": "Add an email or domain to the suppression list (scope is 'email' or 'domain')\n so future campaigns exclude it.",
"inputSchema": {
"properties": {
"reason": {
"title": "Reason",
"type": "string"
},
"scope": {
"title": "Scope",
"type": "string"
},
"value": {
"title": "Value",
"type": "string"
}
},
"required": [
"scope",
"value",
"reason"
],
"title": "add_suppressionArguments",
"type": "object"
},
"name": "add_suppression",
"outputSchema": {
"$defs": {
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
},
"OkResult": {
"properties": {
"ok": {
"title": "Ok",
"type": "boolean"
}
},
"required": [
"ok"
],
"title": "OkResult",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/OkResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "OkOutput"
}
},
{
"description": "Fetch a page of leads produced by a campaign (paginate with offset/limit).\n kept_only=True returns only rows the user marked kept in review (for export).\n match_status narrows to one population: 'validated' (verified email, billed) or\n 'needs_review' (surfaced free; rejection_reason says why) — omit it for both.\n Returns {summary, total, offset, limit, rows}; total counts the filtered\n population. Rows carry verification receipts (verified_by/verified_at/\n verifier_verdict) when a live verification exists — treat them as the\n authoritative proof; null means no receipt, not a failure. Two more quotable\n proof lines appear where earned: person_receipt (who the person is and where we\n saw them launch, on person-target leads) and tech_receipt (dated evidence of the\n detected platform) — forward them as-is.",
"inputSchema": {
"properties": {
"job_id": {
"title": "Job Id",
"type": "string"
},
"kept_only": {
"default": false,
"title": "Kept Only",
"type": "boolean"
},
"limit": {
"default": 100,
"title": "Limit",
"type": "integer"
},
"match_status": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Match Status"
},
"offset": {
"default": 0,
"title": "Offset",
"type": "integer"
}
},
"required": [
"job_id"
],
"title": "fetch_resultsArguments",
"type": "object"
},
"name": "fetch_results",
"outputSchema": {
"$defs": {
"CampaignSummary": {
"additionalProperties": true,
"properties": {
"fake_pipeline": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": null,
"description": "true ONLY in demo/sandbox mode; absent in production. If present and true, the leads are synthetic fixtures — flag to the human, do not treat as real.",
"title": "Fake Pipeline"
},
"flagged_reasons": {
"anyOf": [
{
"additionalProperties": {
"type": "integer"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"title": "Flagged Reasons"
},
"gate_survivors": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "internal diagnostic",
"title": "Gate Survivors"
},
"needs_review": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Needs Review"
},
"removed_reasons": {
"anyOf": [
{
"additionalProperties": {
"type": "integer"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"title": "Removed Reasons"
},
"schema_version": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "internal diagnostic",
"title": "Schema Version"
},
"sourced": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Sourced"
},
"tier_a_matched": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "internal diagnostic",
"title": "Tier A Matched"
},
"total_discovered": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Total Discovered"
}
},
"title": "CampaignSummary",
"type": "object"
},
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
},
"FetchResultsResult": {
"properties": {
"credits_spent": {
"title": "Credits Spent",
"type": "integer"
},
"limit": {
"title": "Limit",
"type": "integer"
},
"offset": {
"title": "Offset",
"type": "integer"
},
"rows": {
"items": {
"$ref": "#/$defs/LeadRow"
},
"title": "Rows",
"type": "array"
},
"summary": {
"anyOf": [
{
"$ref": "#/$defs/CampaignSummary"
},
{
"type": "null"
}
],
"default": null
},
"total": {
"title": "Total",
"type": "integer"
}
},
"required": [
"total",
"offset",
"limit",
"credits_spent",
"rows"
],
"title": "FetchResultsResult",
"type": "object"
},
"LeadRow": {
"additionalProperties": true,
"properties": {
"audience_receipt": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "quotable line saying how much the audience figure is worth when it is NOT a confirmed fact: on TikTok, reported by our data source on a given date, because TikTok publishes no official figure to check it against; on either surface, that the platform disclosed no count at all when subscriber_count is null. Forward it as-is alongside subscriber_count. EMPTY means, and only ever means, that the number is confirmed against the platform's own API (YouTube), so there is nothing to disclaim",
"title": "Audience Receipt"
},
"channel_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "the creator's channel id (YouTube) or numeric profile id (TikTok) — the stable identity the lead was billed against. Never the @handle, which can change",
"title": "Channel Id"
},
"discovery_source": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "which feed surfaced the lead (producthunt / yc / tinylaunch); empty for Maps and web-search leads",
"title": "Discovery Source"
},
"domain": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Domain"
},
"email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Email"
},
"evidence_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "URL of the launch listing where we saw the person/company",
"title": "Evidence Url"
},
"handle": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "the creator's public @handle",
"title": "Handle"
},
"id": {
"title": "Id",
"type": "integer"
},
"last_upload_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "ISO date behind the 'still active' claim. On YouTube it is the channel's most recent upload. On TikTok it is the newest post THIS SEARCH matched, which can only under-state how active the creator is — never over-state it. Empty when the platform did not answer",
"title": "Last Upload At"
},
"launched_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "ISO date the company launched on its feed; empty when the feed lists it without a date (e.g. YC carries a batch instead)",
"title": "Launched At"
},
"match_status": {
"description": "validated (verified email, billable) | needs_review (no verified email or unresolved predicate; may be 'needs_review:<fields>')",
"title": "Match Status",
"type": "string"
},
"niche_receipt": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "quotable proof line for the niche verdict (where the niche appears in the channel's own words, and when we checked) — forward it as-is; empty on an UNKNOWN, because there is no claim to prove",
"title": "Niche Receipt"
},
"niche_state": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "whether the channel's own text confirmed the niche the campaign asked for: MATCH / UNKNOWN. An UNKNOWN row is free and says so via rejection_reason couldnt_confirm:niche_state",
"title": "Niche State"
},
"person_first": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "named person's first name (person-target campaigns)",
"title": "Person First"
},
"person_last": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "named person's last name (person-target campaigns)",
"title": "Person Last"
},
"person_receipt": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "quotable proof line for a person lead: who they are, where we saw them launch, and who verified the email — forward it as-is",
"title": "Person Receipt"
},
"person_role": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "the person's role at the company (e.g. 'founder')",
"title": "Person Role"
},
"platform": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "on a creator lead, the surface the creator lives on ('youtube' or 'tiktok'); on an ordinary business lead, the site builder behind its website (wix / squarespace / shopify / ...). Empty when neither applies",
"title": "Platform"
},
"rejection_reason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "why a row is needs_review, as a stable code: no_verified_email (no mailbox we could verify), couldnt_confirm or couldnt_confirm:<field> (a filter came back unknown; <field> names the *_state signal, e.g. couldnt_confirm:tech_state), no_person_identified (person campaign: we could not name a human), email_not_the_person (person campaign: deliverable address, but it belongs to someone else), email_not_at_company (company campaign: the address lives at a different company's domain), creator_gone (creators campaign: the channel or profile is no longer reachable), subs_out_of_range (creators campaign: the creator's audience is outside the requested subscriber band), subs_unknown (creators campaign: an audience band was requested and the platform does not disclose this creator's count — a YouTube channel that hides it, or a TikTok record with no follower number — so the audience is unconfirmed rather than measured; subscriber_count is null), creator_inactive (creators campaign: nothing posted inside the activity window), couldnt_confirm:niche_state (creators campaign: the creator's own words — channel title/description on YouTube, bio or matched post on TikTok — did not confirm the niche), out_of_credits (balance hit 0 mid-run — top up and re-run to deliver), key_budget_exhausted (the submitting API key's own budget ran out, though the account may still have credit). null when delivered",
"title": "Rejection Reason"
},
"review_status": {
"enum": [
"kept",
"discarded",
"unreviewed"
],
"title": "Review Status",
"type": "string"
},
"subscriber_count": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "the creator's audience — and where it comes from differs by platform, which matters if you forward it. On YOUTUBE it is the platform's own official API figure (never a scraped number). On TIKTOK no free official figure exists, so this is what our data source reported and nothing independent confirmed it; audience_receipt carries that attribution in the customer's own words. Either way it is the figure the subscriber-band claim was checked against. NULL means the platform did not disclose a count at all (a YouTube channel that hides it, a TikTok record with no follower number) — never read that as zero, and see rejection_reason subs_unknown",
"title": "Subscriber Count"
},
"tech_receipt": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "quotable proof line for the detected platform (dated evidence from the crawl) — forward it as-is",
"title": "Tech Receipt"
},
"tech_state": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "platform-membership verdict from the site crawl: TECH_DETECTED / TECH_NONE_DETECTED; empty when the site could not be crawled",
"title": "Tech State"
},
"verified_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "ISO timestamp of the verification",
"title": "Verified At"
},
"verified_by": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "who verified the email (e.g. 'zerobounce'); null = no live verification receipt. With verified_at/verifier_verdict this is the proof-of-verification you can log or forward — a lead with verified_by set was checked against the live mailbox provider, not a cached guess.",
"title": "Verified By"
},
"verifier_verdict": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "verifier's raw verdict (e.g. 'valid')",
"title": "Verifier Verdict"
}
},
"required": [
"id",
"match_status",
"review_status"
],
"title": "LeadRow",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/FetchResultsResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "FetchResultsOutput"
}
},
{
"description": "Check the status of a previously submitted campaign (queued/running/done/\n done_partial/failed) by job_id. Prefer wait_seconds=45 over sleeping between\n polls: the call holds until the status or pipeline stage changes (or the timer\n expires) and returns the normal snapshot either way — loop on it until `status`\n is terminal. Out-of-range wait_seconds clamps to [0, 45], never errors.",
"inputSchema": {
"properties": {
"job_id": {
"title": "Job Id",
"type": "string"
},
"wait_seconds": {
"default": 0,
"title": "Wait Seconds",
"type": "integer"
}
},
"required": [
"job_id"
],
"title": "get_campaign_statusArguments",
"type": "object"
},
"name": "get_campaign_status",
"outputSchema": {
"$defs": {
"CampaignStatusResult": {
"additionalProperties": true,
"properties": {
"credits_spent": {
"description": "credits billed for this job so far (1 per validated lead; 2 per validated lead for person-target or Meta-ads campaigns)",
"title": "Credits Spent",
"type": "integer"
},
"status": {
"description": "done_partial = finished but stopped early — status_reason says why; delivered results are still valid. failed = no results; status_reason says why.",
"enum": [
"queued",
"running",
"done",
"done_partial",
"failed"
],
"title": "Status",
"type": "string"
},
"status_reason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "human-readable reason the job ended done_partial or failed (e.g. credits ran out, a processing limit was hit, time limit). null while queued/running and on a clean done.",
"title": "Status Reason"
},
"summary": {
"anyOf": [
{
"$ref": "#/$defs/CampaignSummary"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"status",
"credits_spent"
],
"title": "CampaignStatusResult",
"type": "object"
},
"CampaignSummary": {
"additionalProperties": true,
"properties": {
"fake_pipeline": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": null,
"description": "true ONLY in demo/sandbox mode; absent in production. If present and true, the leads are synthetic fixtures — flag to the human, do not treat as real.",
"title": "Fake Pipeline"
},
"flagged_reasons": {
"anyOf": [
{
"additionalProperties": {
"type": "integer"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"title": "Flagged Reasons"
},
"gate_survivors": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "internal diagnostic",
"title": "Gate Survivors"
},
"needs_review": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Needs Review"
},
"removed_reasons": {
"anyOf": [
{
"additionalProperties": {
"type": "integer"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"title": "Removed Reasons"
},
"schema_version": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "internal diagnostic",
"title": "Schema Version"
},
"sourced": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Sourced"
},
"tier_a_matched": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "internal diagnostic",
"title": "Tier A Matched"
},
"total_discovered": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Total Discovered"
}
},
"title": "CampaignSummary",
"type": "object"
},
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/CampaignStatusResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "CampaignStatusOutput"
}
},
{
"description": "Check this tenant's prepaid credit balance (a validated lead costs 1 credit —\n 2 for person-target or Meta-ads campaigns), the available credit packs, and the\n ledger (paginate ledger entries with offset/limit, newest first). Campaigns\n cannot start with a zero balance — buy a pack via the dashboard when balance\n runs low.\n When you authenticate with an API key this also returns YOUR key's remaining\n budgets — check it before large campaigns.",
"inputSchema": {
"properties": {
"limit": {
"default": 50,
"title": "Limit",
"type": "integer"
},
"offset": {
"default": 0,
"title": "Offset",
"type": "integer"
}
},
"title": "get_creditsArguments",
"type": "object"
},
"name": "get_credits",
"outputSchema": {
"$defs": {
"CreditsResult": {
"additionalProperties": true,
"properties": {
"balance": {
"title": "Balance",
"type": "integer"
},
"ledger": {
"items": {
"additionalProperties": true,
"type": "object"
},
"title": "Ledger",
"type": "array"
},
"packs": {
"items": {
"additionalProperties": true,
"type": "object"
},
"title": "Packs",
"type": "array"
}
},
"required": [
"balance",
"packs",
"ledger"
],
"title": "CreditsResult",
"type": "object"
},
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/CreditsResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "CreditsOutput"
}
},
{
"description": "List the ICP packs (saved vertical/geo templates) available to this tenant.",
"inputSchema": {
"properties": {},
"title": "get_icp_packArguments",
"type": "object"
},
"name": "get_icp_pack",
"outputSchema": {
"$defs": {
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
},
"IcpPackResult": {
"properties": {
"packs": {
"description": "saved vertical/geo templates; empty list is normal for a tenant with none — not an error",
"items": {
"additionalProperties": true,
"type": "object"
},
"title": "Packs",
"type": "array"
}
},
"required": [
"packs"
],
"title": "IcpPackResult",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/IcpPackResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "IcpPackOutput"
}
},
{
"description": "Today's external vendor usage against the tenant's daily quota\n (vendor_calls_today / per_day_max / remaining). This is the OTHER ceiling besides\n credits: when remaining hits 0, campaign work pauses until midnight even with a\n healthy credit balance — check it before launching a large or urgent campaign.\n `remaining` is quota UNITS, which usually equals calls; a creator discovery costs\n one unit per delivered record, so it can consume far more than one call's worth.\n Mirrors REST GET /v1/usage.",
"inputSchema": {
"properties": {},
"title": "get_usageArguments",
"type": "object"
},
"name": "get_usage",
"outputSchema": {
"$defs": {
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
},
"UsageResult": {
"properties": {
"per_day_max": {
"description": "the tenant's daily vendor quota, in units",
"title": "Per Day Max",
"type": "integer"
},
"remaining": {
"description": "quota units left today; at 0 new campaign work pauses until midnight (quota, not credits — buying credits does not raise it). Usually equal to per_day_max minus vendor_calls_today, since most vendor calls cost one unit; a creator discovery costs one unit per delivered record, so it can consume far more than the one call it looks like",
"title": "Remaining",
"type": "integer"
},
"vendor_calls_today": {
"description": "external vendor calls (discovery, crawl, verification, enrichment) made for this tenant since midnight",
"title": "Vendor Calls Today",
"type": "integer"
}
},
"required": [
"vendor_calls_today",
"per_day_max",
"remaining"
],
"title": "UsageResult",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/UsageResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "UsageOutput"
}
},
{
"description": "List this tenant's campaigns, newest first — the recovery path when a job_id\n was lost: every row carries job_id, status, the query it ran, and credits spent,\n so you can resume polling (get_campaign_status) or fetch results without\n resubmitting (and double-charging). Filters: q (substring over vertical/geo),\n status (queued | running | done | done_partial | failed), created_after/\n created_before (ISO-8601 window over creation time), order ('desc' default,\n 'asc' oldest first). Paginate with offset/limit (limit 1-200); `total` counts\n everything matching the filters, not just this page.",
"inputSchema": {
"properties": {
"created_after": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Created After"
},
"created_before": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Created Before"
},
"limit": {
"default": 50,
"title": "Limit",
"type": "integer"
},
"offset": {
"default": 0,
"title": "Offset",
"type": "integer"
},
"order": {
"default": "desc",
"title": "Order",
"type": "string"
},
"q": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Q"
},
"status": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Status"
}
},
"title": "list_campaignsArguments",
"type": "object"
},
"name": "list_campaigns",
"outputSchema": {
"$defs": {
"CampaignListItem": {
"additionalProperties": true,
"properties": {
"agent_label": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "label of the API key that submitted it; null for dashboard-submitted campaigns",
"title": "Agent Label"
},
"created_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Created At"
},
"credits_spent": {
"default": 0,
"description": "credits billed for this campaign (from the ledger; 1 per validated lead, 2 for person-target or Meta-ads campaigns)",
"title": "Credits Spent",
"type": "integer"
},
"finished_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Finished At"
},
"geo": {
"default": "",
"title": "Geo",
"type": "string"
},
"job_id": {
"title": "Job Id",
"type": "string"
},
"query": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "the structured query the campaign ran (including any `discovery` block) — reuse it to resubmit or refine",
"title": "Query"
},
"status": {
"enum": [
"queued",
"running",
"done",
"done_partial",
"failed"
],
"title": "Status",
"type": "string"
},
"status_reason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "why the campaign ended done_partial or failed; null otherwise",
"title": "Status Reason"
},
"summary": {
"anyOf": [
{
"$ref": "#/$defs/CampaignSummary"
},
{
"type": "null"
}
],
"default": null
},
"vertical": {
"default": "",
"description": "empty for a locationless campaign (launch_feeds / web_search discovery)",
"title": "Vertical",
"type": "string"
}
},
"required": [
"job_id",
"status"
],
"title": "CampaignListItem",
"type": "object"
},
"CampaignSummary": {
"additionalProperties": true,
"properties": {
"fake_pipeline": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": null,
"description": "true ONLY in demo/sandbox mode; absent in production. If present and true, the leads are synthetic fixtures — flag to the human, do not treat as real.",
"title": "Fake Pipeline"
},
"flagged_reasons": {
"anyOf": [
{
"additionalProperties": {
"type": "integer"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"title": "Flagged Reasons"
},
"gate_survivors": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "internal diagnostic",
"title": "Gate Survivors"
},
"needs_review": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Needs Review"
},
"removed_reasons": {
"anyOf": [
{
"additionalProperties": {
"type": "integer"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"title": "Removed Reasons"
},
"schema_version": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "internal diagnostic",
"title": "Schema Version"
},
"sourced": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Sourced"
},
"tier_a_matched": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "internal diagnostic",
"title": "Tier A Matched"
},
"total_discovered": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Total Discovered"
}
},
"title": "CampaignSummary",
"type": "object"
},
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
},
"ListCampaignsResult": {
"properties": {
"campaigns": {
"items": {
"$ref": "#/$defs/CampaignListItem"
},
"title": "Campaigns",
"type": "array"
},
"limit": {
"title": "Limit",
"type": "integer"
},
"offset": {
"title": "Offset",
"type": "integer"
},
"total": {
"description": "campaigns matching the filters (not just this page)",
"title": "Total",
"type": "integer"
}
},
"required": [
"total",
"offset",
"limit",
"campaigns"
],
"title": "ListCampaignsResult",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/ListCampaignsResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "ListCampaignsOutput"
}
},
{
"description": "Mark a lead kept or discarded after review (status: kept | discarded |\n unreviewed). Then fetch_results(kept_only=true) returns only kept leads — the\n export set.",
"inputSchema": {
"properties": {
"job_id": {
"title": "Job Id",
"type": "string"
},
"result_id": {
"title": "Result Id",
"type": "integer"
},
"status": {
"title": "Status",
"type": "string"
}
},
"required": [
"job_id",
"result_id",
"status"
],
"title": "review_leadArguments",
"type": "object"
},
"name": "review_lead",
"outputSchema": {
"$defs": {
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
},
"ReviewLeadResult": {
"properties": {
"job_id": {
"title": "Job Id",
"type": "string"
},
"ok": {
"title": "Ok",
"type": "boolean"
},
"result_id": {
"title": "Result Id",
"type": "integer"
},
"status": {
"enum": [
"kept",
"discarded",
"unreviewed"
],
"title": "Status",
"type": "string"
}
},
"required": [
"ok",
"job_id",
"result_id",
"status"
],
"title": "ReviewLeadResult",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/ReviewLeadResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "ReviewLeadOutput"
}
},
{
"description": "Send feedback to the Nose for Leads team. Set kind='product' for feedback\n about the leads, the service, coverage, or pricing — relay what the business\n owner tells you (categories: lead_quality, pricing, missing_feature, praise,\n other). Set kind='tool' (default) for feedback about these MCP tools\n themselves — a confusing description, a bug, a missing capability. Use freely;\n it's our main signal for improving the product and the agent experience.",
"inputSchema": {
"properties": {
"category": {
"default": "other",
"enum": [
"bug",
"confusing",
"missing_feature",
"lead_quality",
"pricing",
"praise",
"other"
],
"title": "Category",
"type": "string"
},
"kind": {
"default": "tool",
"enum": [
"tool",
"product"
],
"title": "Kind",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
},
"tool": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Tool"
}
},
"required": [
"message"
],
"title": "send_feedbackArguments",
"type": "object"
},
"name": "send_feedback",
"outputSchema": {
"$defs": {
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
},
"FeedbackResult": {
"properties": {
"ok": {
"title": "Ok",
"type": "boolean"
}
},
"required": [
"ok"
],
"title": "FeedbackResult",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/FeedbackResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "FeedbackOutput"
}
},
{
"description": "Submit a lead-generation campaign. Pass translate_icp's `vertical`, `geo`, and\n `query` fields directly (category/location are ALIASES for vertical/geo, accepted\n so translate output round-trips; prefer vertical/geo when constructing calls).\n `query`'s full shape and field vocabulary are in this tool's schema — build it\n with translate_icp from free text (recommended), or construct it directly. An\n unknown predicate field (or rank), and any unknown `discovery` key, is rejected\n with a 422 naming it; an unknown top-level `query` key is silently ignored, so\n keep to predicates/rank/discovery — a typo there will not error, it will just do\n nothing.\n Costs 1 credit per validated lead — every campaign type, including founder/person\n campaigns and campaigns that filter on Meta ads. A campaign cannot overspend: if\n discovered leads exceed your balance it delivers what your credits cover and the\n campaign ends `done_partial`; check get_credits first if your balance is low.\n vertical/geo are REQUIRED except for a locationless campaign — `query` carrying a\n discovery block with mode 'launch_feeds' or 'web_search' — which has no geography\n at all.\n If the tenant has no card on file this returns a 403 `card_required` error with a\n `setup_url` — surface that URL to the user so they can verify a card (never\n charged unless they buy a pack).\n Pass a stable idempotency_key when you might retry — a retry returns the ORIGINAL\n campaign (idempotent_replay=true), never a duplicate charge.\n Returns {job_id, idempotent_replay}; poll get_campaign_status with job_id.",
"inputSchema": {
"$defs": {
"QueryInput": {
"additionalProperties": true,
"description": "The structured filter start_campaign runs. Build it with translate_icp from free\ntext (recommended), or construct it directly from this schema's vocabulary.",
"properties": {
"discovery": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "OPTIONAL non-Maps discovery directive. Omit it (the default) to discover local businesses on Google Maps, which is what vertical/geo describe. Two other modes exist, both with NO geography (vertical/geo are then not required). mode \"launch_feeds\": companies that RECENTLY LAUNCHED — and, optionally, the named founders behind them — from Product Hunt, Y Combinator and TinyLaunch; keys target_type (\"person\" for named founders or \"company\"; both bill 1 credit per validated lead), role (default \"founder\"), launch_window_days (1-365, default 90), category_keywords (e.g. [\"b2b\", \"saas\"]; empty keeps everything). mode \"web_search\": long-tail ONLINE businesses Google Maps never had, found by organic search — keys search_queries (1-5 queries, each a separate paid search; say what the business IS, e.g. [\"marketing agencies for dentists\"]), search_location (search locale, default \"United States\"), search_depth (10-100, default 60). It finds COMPANIES ONLY (billed 1 credit) and cannot carry the city-keyed signals runs_google_ads / serp_rank / runs_meta_ads / in_local_pack / ads_state. Filter platform membership with the detected_tech and tech_state fields instead. Firmographics (funding stage, headcount, revenue, technology spend) are NOT available in any mode and must never be written into a search query. Prefer translate_icp, which emits this block for you; a malformed or unknown-mode block is rejected at submit with a 422 naming the problem, never silently run as a Maps campaign. A mode may also be temporarily unavailable when its costs are not covered: submitting it returns a 422 that says so, and nothing is charged.",
"title": "Discovery"
},
"predicates": {
"description": "all predicates must hold for a lead to count as a confirmed match",
"items": {
"$ref": "#/$defs/QueryPredicateInput"
},
"title": "Predicates",
"type": "array"
},
"rank": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "optional field to sort/prioritize results by (same vocabulary as predicate fields)",
"enum": [
"ads_state",
"areas_served",
"booking_state",
"category",
"detected_tech",
"digital_maturity_score",
"emergency_24_7",
"first_review_at",
"has_contact_form",
"has_h1",
"has_hours",
"has_meta_description",
"has_real_site",
"has_robots",
"has_schema_org",
"has_sitemap",
"has_title",
"has_viewport",
"has_website",
"in_local_pack",
"is_claimed",
"is_franchise",
"is_social_only",
"licensed",
"locations_in_campaign",
"low_star_reviews_30d",
"low_star_reviews_60d",
"low_star_reviews_90d",
"multi_location",
"outdated_state",
"page_count",
"platform",
"rating",
"review_count",
"review_trend_state",
"runs_google_ads",
"runs_meta_ads",
"seo_basics_state",
"serp_rank",
"tech_state",
"titles_identical",
"total_photos",
"website_social_only",
"website_state",
"years_in_business"
],
"title": "Rank"
}
},
"title": "QueryInput",
"type": "object"
},
"QueryPredicateInput": {
"additionalProperties": true,
"properties": {
"field": {
"description": "the business attribute to filter on. The enum is the supported vocabulary; x-field-catalog documents each field's meaning, type, allowed ops, an example value, and — for the *_state verdict fields — the exact values to compare against. An unknown predicate field is rejected at submit with a 422 naming it.",
"enum": [
"ads_state",
"areas_served",
"booking_state",
"category",
"detected_tech",
"digital_maturity_score",
"emergency_24_7",
"first_review_at",
"has_contact_form",
"has_h1",
"has_hours",
"has_meta_description",
"has_real_site",
"has_robots",
"has_schema_org",
"has_sitemap",
"has_title",
"has_viewport",
"has_website",
"in_local_pack",
"is_claimed",
"is_franchise",
"is_social_only",
"licensed",
"locations_in_campaign",
"low_star_reviews_30d",
"low_star_reviews_60d",
"low_star_reviews_90d",
"multi_location",
"outdated_state",
"page_count",
"platform",
"rating",
"review_count",
"review_trend_state",
"runs_google_ads",
"runs_meta_ads",
"seo_basics_state",
"serp_rank",
"tech_state",
"titles_identical",
"total_photos",
"website_social_only",
"website_state",
"years_in_business"
],
"title": "Field",
"type": "string",
"x-field-catalog": {
"ads_state": {
"description": "paid-ads verdict ADS_PRESENT/NO_ADS_FOUND, null when unresolved",
"example": "NO_ADS_FOUND",
"ops": [
"eq",
"neq",
"exists"
],
"type": "str",
"values": [
"ADS_PRESENT",
"NO_ADS_FOUND"
]
},
"areas_served": {
"description": "service-area city names parsed from the site",
"example": "Phoenix",
"ops": [
"contains",
"not_contains"
],
"type": "list[str]"
},
"booking_state": {
"description": "online-booking verdict BOOKING_PRESENT/BOOKING_ABSENT, null when unresolved",
"example": "BOOKING_ABSENT",
"ops": [
"eq",
"neq",
"exists"
],
"type": "str",
"values": [
"BOOKING_PRESENT",
"BOOKING_ABSENT"
]
},
"category": {
"description": "the business vertical/type — goes in the top-level `category`, NOT a predicate",
"example": "plumber",
"ops": [
"eq"
],
"type": "str"
},
"detected_tech": {
"description": "tech/widgets detected on the site (e.g. shopify, calendly, toast, doordash)",
"example": "shopify",
"ops": [
"contains",
"not_contains"
],
"type": "list[str]"
},
"digital_maturity_score": {
"description": "0-100 composite web-maturity score (low = bigger digital gap)",
"example": 50,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq",
"neq"
],
"type": "float"
},
"emergency_24_7": {
"description": "site advertises 24/7 emergency service",
"example": true,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"first_review_at": {
"description": "the date of the business's EARLIEST Google review (ISO, e.g. 2024-01-15) -- only set when the reviews we fetched are provably its ENTIRE review history. This is an AGE FLOOR ('has existed at least since this date'), never an opening date: a long-established business that only recently got its first review looks new by this measure. Blank/null when we only saw part of its history and cannot know the true first review -- a real, unbilled outcome, not an error. Use this to answer how-long-has-this-business-existed style asks by comparing against a cutoff date; never present it as a founding or opening date",
"example": "2024-01-15",
"ops": [
"eq",
"neq",
"lt",
"lte",
"gt",
"gte",
"exists"
],
"type": "str"
},
"has_contact_form": {
"description": "the site has a contact/inquiry form",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"has_h1": {
"description": "the homepage has a non-empty <h1> heading, read from raw served HTML (may read absent on client-rendered sites; prefer the seo_basics segment for render-safe SEO health)",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"has_hours": {
"description": "business lists opening hours",
"example": true,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"has_meta_description": {
"description": "the homepage has a meta description (basic SEO)",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"has_real_site": {
"description": "has a real website (not social-only / not empty)",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"has_robots": {
"description": "the site has a /robots.txt, read from raw served HTML (may read absent on client-rendered sites; prefer the seo_basics segment for render-safe SEO health)",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"has_schema_org": {
"description": "the site has schema.org structured data",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"has_sitemap": {
"description": "the site has a /sitemap.xml, read from raw served HTML (may read absent on client-rendered sites; prefer the seo_basics segment for render-safe SEO health)",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"has_title": {
"description": "the homepage has a non-empty <title>",
"example": true,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"has_viewport": {
"description": "the site is mobile-friendly (has a viewport meta tag)",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"has_website": {
"description": "business has any website at all",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"in_local_pack": {
"description": "appears in the Google Maps local 3-pack for its category+city",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"is_claimed": {
"description": "the Google listing is claimed/managed by the owner",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"is_franchise": {
"description": "the business name matches a known national franchise brand (dated list); False means no match on this list, NOT verified independent — never claim independence from this field alone",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"is_social_only": {
"description": "the only web presence is a social page (Facebook/Instagram/etc.)",
"example": true,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"licensed": {
"description": "site states licensed/insured",
"example": true,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"locations_in_campaign": {
"description": "how many businesses in this search share this name or domain at different addresses (>1 = multi-location/chain in this run)",
"example": 3,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq",
"neq"
],
"type": "int"
},
"low_star_reviews_30d": {
"description": "how many of this business's Google reviews are rated 2 stars or less, counting back 30 days from the date we FETCHED its reviews (not today's date). null means we could not prove we saw the whole 30-day window -- a real, unbilled outcome, not an error, and NOT the same as a proven zero (zero means we saw the whole window and it was clean). For a plain yes/no 'does this business have a reputation problem' ask, use review_trend_state instead of a raw count",
"example": 3,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq",
"neq"
],
"type": "int"
},
"low_star_reviews_60d": {
"description": "how many of this business's Google reviews are rated 2 stars or less, counting back 60 days from the date we FETCHED its reviews (not today's date). null means we could not prove we saw the whole 60-day window -- a real, unbilled outcome, not an error, and NOT the same as a proven zero (zero means we saw the whole window and it was clean). For a plain yes/no 'does this business have a reputation problem' ask, use review_trend_state instead of a raw count",
"example": 3,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq",
"neq"
],
"type": "int"
},
"low_star_reviews_90d": {
"description": "how many of this business's Google reviews are rated 2 stars or less, counting back 90 days from the date we FETCHED its reviews (not today's date). null means we could not prove we saw the whole 90-day window -- a real, unbilled outcome, not an error, and NOT the same as a proven zero (zero means we saw the whole window and it was clean). For a plain yes/no 'does this business have a reputation problem' ask, use review_trend_state instead of a raw count",
"example": 3,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq",
"neq"
],
"type": "int"
},
"multi_location": {
"description": "the same business record was found in multiple data sources and merged into one canonical record during dedupe (a dedupe multiplicity) — NOT chain/multi-site detection; use locations_in_campaign for multi-location/chain filtering",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"outdated_state": {
"description": "outdated-website verdict OUTDATED/CURRENT, null when unresolved",
"example": "OUTDATED",
"ops": [
"eq",
"neq",
"exists"
],
"type": "str",
"values": [
"OUTDATED",
"CURRENT"
]
},
"page_count": {
"description": "how many of home/contact/about/team pages resolved",
"example": 1,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq",
"neq"
],
"type": "int"
},
"platform": {
"description": "site builder/platform (wix, squarespace, wordpress, godaddy, weebly, shopify, custom)",
"example": "wix",
"ops": [
"eq",
"neq"
],
"type": "str"
},
"rating": {
"description": "average star rating, 0-5",
"example": 4.2,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq",
"neq"
],
"type": "float"
},
"review_count": {
"description": "number of reviews",
"example": 30,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq",
"neq"
],
"type": "int"
},
"review_trend_state": {
"description": "recent-reviews verdict — NEGATIVE_WAVE when the business received 3 or more reviews rated 2 stars or less in the last 90 days, STABLE when it did not, null when we could not see the whole 90-day window (a real, unbilled outcome — not an error, and not the same as STABLE). Use this for 'recent bad reviews', 'wave of 1-star reviews', 'reputation problems lately'. It is about RECENT ratings only — for overall rating or review volume use rating / review_count instead",
"example": "NEGATIVE_WAVE",
"ops": [
"eq",
"neq",
"exists"
],
"type": "str"
},
"runs_google_ads": {
"description": "the business runs Google Search ads (advertiser-level, any campaign)",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"runs_meta_ads": {
"description": "the business runs active Meta/Facebook ads (Ad Library). EXPERIMENTAL: the resolver is unverified and OFF by default (needs --enable-meta); without it this yields needs_review, never a confident answer",
"example": false,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"seo_basics_state": {
"description": "render-safe SEO health verdict: SEO_BASICS_MISSING (2+ of no title/no meta description/no H1/identical titles/no sitemap+robots) or SEO_BASICS_PRESENT; null when unresolved or the site is client-rendered (title/meta/H1 injected at runtime, so raw HTML can't be trusted) — prefer this over the raw has_title/has_h1/etc. fields for SEO-health filtering",
"example": "SEO_BASICS_MISSING",
"ops": [
"eq",
"neq",
"exists"
],
"type": "str",
"values": [
"SEO_BASICS_MISSING",
"SEO_BASICS_PRESENT"
]
},
"serp_rank": {
"description": "Google organic position for its category+city, 1=top. Prefer page thresholds: <=10 'page 1', <=50 'top 50', >10 'not on page 1', eq 1 'ranked #1'. Never emit a value above 100 or compare against the not-ranked sentinel. Approximate (keyword-proxy): a business may rank for a narrower term than category+city, so 'not ranking' can under-count.",
"example": 10,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq"
],
"type": "int"
},
"tech_state": {
"description": "platform-membership verdict from the site crawl — TECH_DETECTED / TECH_NONE_DETECTED, null when the site could not be crawled or is client-rendered (a runtime-injected widget is invisible in served HTML). Pair it with detected_tech to name the platform",
"example": "TECH_DETECTED",
"ops": [
"eq",
"neq",
"exists"
],
"type": "str",
"values": [
"TECH_DETECTED",
"TECH_NONE_DETECTED"
]
},
"titles_identical": {
"description": "the <title> tag is identical across every crawled page (thin/templated-site smell), read from raw served HTML (may read absent on client-rendered sites; prefer the seo_basics segment for render-safe SEO health)",
"example": true,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"total_photos": {
"description": "number of listing photos",
"example": 10,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq",
"neq"
],
"type": "int"
},
"website_social_only": {
"description": "the business's only web presence is a social/directory page like Facebook, no real website",
"example": true,
"ops": [
"eq",
"exists"
],
"type": "bool"
},
"website_state": {
"description": "website-presence verdict — WEBSITE_NONE / WEBSITE_SOCIAL_ONLY / WEBSITE_PRESENT, null when unresolved",
"example": "WEBSITE_NONE",
"ops": [
"eq",
"neq",
"exists"
],
"type": "str",
"values": [
"WEBSITE_NONE",
"WEBSITE_SOCIAL_ONLY",
"WEBSITE_PRESENT"
]
},
"years_in_business": {
"description": "the FOUNDING YEAR (a 4-digit year like 1998), parsed from 'since/established YYYY' — NOT a duration. To find businesses younger than N years, use years_in_business > (current_year - N); older than N, use < that value",
"example": 1998,
"ops": [
"lt",
"lte",
"gt",
"gte",
"eq",
"neq"
],
"type": "int"
}
}
},
"op": {
"description": "comparison operator",
"enum": [
"eq",
"neq",
"lt",
"lte",
"gt",
"gte",
"contains",
"not_contains",
"in",
"exists"
],
"title": "Op",
"type": "string"
},
"value": {
"default": null,
"description": "comparison value; omit for op='exists'",
"title": "Value"
}
},
"required": [
"field",
"op"
],
"title": "QueryPredicateInput",
"type": "object"
}
},
"properties": {
"category": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Category"
},
"geo": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Geo"
},
"idempotency_key": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Idempotency Key"
},
"location": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Location"
},
"query": {
"$ref": "#/$defs/QueryInput"
},
"vertical": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Vertical"
}
},
"required": [
"query"
],
"title": "start_campaignArguments",
"type": "object"
},
"name": "start_campaign",
"outputSchema": {
"$defs": {
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
},
"StartCampaignResult": {
"properties": {
"idempotent_replay": {
"default": false,
"title": "Idempotent Replay",
"type": "boolean"
},
"job_id": {
"title": "Job Id",
"type": "string"
}
},
"required": [
"job_id"
],
"title": "StartCampaignResult",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/StartCampaignResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "StartCampaignOutput"
}
},
{
"description": "Translate free-text ICP description (e.g. \"plumbers in phoenix with no\n website\") into a structured query. Call this BEFORE start_campaign — start_campaign\n only accepts a structured `query` dict, never free text. Returns {query, vertical,\n geo, category, location, preview, unsupported, discovery} — pass vertical/geo/query\n straight to start_campaign.\n `discovery` is non-empty when the ask routes OUTSIDE Google Maps, in one of two\n shapes. Mode 'launch_feeds': people/companies that RECENTLY LAUNCHED (e.g.\n \"founders of B2B SaaS that launched in the last 3 months\") — it names the launch\n feeds to search and whether the leads are people or companies. Mode 'web_search':\n long-tail ONLINE businesses Google Maps never had (e.g. \"marketing agencies for\n dentists\"), found via paid organic search — companies only, never people. Either\n way the block is already embedded inside the returned `query`, so passing `query`\n through unchanged is all that's needed; neither mode has a geography, so\n vertical/geo come back empty and start_campaign does not require them.\n Anything we cannot actually source lands in `unsupported` instead — read that list\n aloud to the user rather than pretending the campaign will cover it.",
"inputSchema": {
"properties": {
"text": {
"title": "Text",
"type": "string"
}
},
"required": [
"text"
],
"title": "translate_icpArguments",
"type": "object"
},
"name": "translate_icp",
"outputSchema": {
"$defs": {
"ErrorEnvelope": {
"additionalProperties": true,
"description": "Typed error: `code` is a stable machine key (auth_failed, not_found, invalid_input,\npayment_required, internal_error, ...); `message` is human-readable.\n\n`extra=\"allow\"` (card-check gate, spec 2026-07-10 WS-A A1): `_envelope` in\nmcp_server.py adds an optional `setup_url` key ONLY for CardRequired — every other\nerror dict still carries just {code, message}, so without this config pydantic's\ndefault `extra=\"ignore\"` would silently strip setup_url on model_validate, before it\never reached model_dump. A declared `setup_url: str | None = None` field instead was\nruled out: FastMCP's convert_result calls `model_dump(mode=\"json\")` with no\nexclude_none, so a declared-but-unset field would surface as `\"setup_url\": null` on\nEVERY other error and break the existing exact-envelope-shape tests.",
"properties": {
"code": {
"title": "Code",
"type": "string"
},
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"code",
"message"
],
"title": "ErrorEnvelope",
"type": "object"
},
"TranslateResult": {
"additionalProperties": true,
"properties": {
"category": {
"title": "Category",
"type": "string"
},
"discovery": {
"additionalProperties": true,
"description": "the non-Maps discovery directive ({} for an ordinary Maps ask): mode 'launch_feeds', 'web_search' or 'creators' plus its settings. Informational only — it is already embedded in `query`, so pass `query` through to start_campaign unchanged; never re-attach this block yourself.",
"title": "Discovery",
"type": "object"
},
"geo": {
"description": "Pass to start_campaign as geo",
"title": "Geo",
"type": "string"
},
"location": {
"title": "Location",
"type": "string"
},
"preview": {
"title": "Preview",
"type": "string"
},
"query": {
"additionalProperties": true,
"title": "Query",
"type": "object"
},
"suggested_rewrite": {
"default": "",
"description": "An honest counter-offer when a refused item (e.g. an off-limits platform like LinkedIn) has a servable alternative (e.g. Instagram). Empty when there is nothing to offer. This is a suggestion only — we never run it for you; the customer must ask for it explicitly.",
"title": "Suggested Rewrite",
"type": "string"
},
"unsupported": {
"items": {
"type": "string"
},
"title": "Unsupported",
"type": "array"
},
"vertical": {
"description": "Pass to start_campaign as vertical",
"title": "Vertical",
"type": "string"
}
},
"required": [
"query",
"vertical",
"geo",
"category",
"location",
"preview",
"unsupported"
],
"title": "TranslateResult",
"type": "object"
}
},
"anyOf": [
{
"$ref": "#/$defs/TranslateResult"
},
{
"$ref": "#/$defs/ErrorEnvelope"
}
],
"title": "TranslateOutput"
}
}
]
}Verify it yourself
curl -s https://api.teppi.xyz/v1/evidence/sha256:87cbd089406d891b6508b411ed06ea14d2d9c7ba2893ca970a32ad130b905df8 | sha256sum