MCP 서버

Nose for Leads

com.noseforleads/leads-mcp
마케팅 및 광고 영업 및 CRM 공개 · 연결 가능 MCP 2025-11-25

이 MCP로 할 수 있는 일

Runs structured local-business lead campaigns, translates ICP descriptions into queries, verifies lead emails, supports review and suppression workflows, and exports selected leads.

add_suppression
Add to suppression list
Add an email or domain to the suppression list (scope is 'email' or 'domain') so future campaigns exclude it.
멱등성
입력 스키마
{'type': 'object', 'title': 'add_suppressionArguments', 'required': ['scope', 'value', 'reason'], 'properties': {'scope': {'type': 'string', 'title': 'Scope'}, 'value': {'type': 'string', 'title': 'Value'}, 'reason': {'type': 'string', 'title': 'Reason'}}}
출력 스키마
{'$defs': {'OkResult': {'type': 'object', 'title': 'OkResult', 'required': ['ok'], 'properties': {'ok': {'type': 'boolean', 'title': 'Ok'}}}, 'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}}, 'anyOf': [{'$ref': '#/$defs/OkResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'OkOutput'}
fetch_results
Fetch campaign results
Fetch a page of leads produced by a campaign (paginate with offset/limit). kept_only=True returns only rows the user marked kept in review (for export). match_status narrows to one population: 'validated' (verified email, billed) or 'needs_review' (surfaced free; rejection_reason says why) — omit it for both. Returns {summary, total, offset, limit, rows}; total counts the filtered population. Rows carry verification receipts (verified_by/verified_at/ verifier_verdict) when a live verification exists — treat them as the authoritative proof; null means no receipt, not a failure. Two more quotable proof lines appear where earned: person_receipt (who the person is and where we saw them launch, on person-target leads) and tech_receipt (dated evidence of the detected platform) — forward them as-is.
읽기 전용
입력 스키마
{'type': 'object', 'title': 'fetch_resultsArguments', 'required': ['job_id'], 'properties': {'limit': {'type': 'integer', 'title': 'Limit', 'default': 100}, 'job_id': {'type': 'string', 'title': 'Job Id'}, 'offset': {'type': 'integer', 'title': 'Offset', 'default': 0}, 'kept_only': {'type': 'boolean', 'title': 'Kept Only', 'default': False}, 'match_status': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Match Status', 'default': None}}}
출력 스키마
{'$defs': {'LeadRow': {'type': 'object', 'title': 'LeadRow', 'required': ['id', 'match_status', 'review_status'], 'properties': {'id': {'type': 'integer', 'title': 'Id'}, 'email': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Email', 'default': None}, 'domain': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Domain', 'default': None}, 'handle': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Handle', 'default': None, 'description': "the creator's public @handle"}, 'platform': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Platform', 'default': None, '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"}, 'channel_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Channel Id', 'default': None, '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"}, 'tech_state': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Tech State', 'default': None, 'description': 'platform-membership verdict from the site crawl: TECH_DETECTED / TECH_NONE_DETECTED; empty when the site could not be crawled'}, 'launched_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Launched At', 'default': None, '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)'}, 'niche_state': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Niche State', 'default': None, '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"}, 'person_last': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Person Last', 'default': None, 'description': "named person's last name (person-target campaigns)"}, 'person_role': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Person Role', 'default': None, 'description': "the person's role at the company (e.g. 'founder')"}, 'verified_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Verified At', 'default': None, 'description': 'ISO timestamp of the verification'}, 'verified_by': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Verified By', 'default': None, '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."}, 'evidence_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Evidence Url', 'default': None, 'description': 'URL of the launch listing where we saw the person/company'}, 'match_status': {'type': 'string', 'title': 'Match Status', 'description': "validated (verified email, billable) | needs_review (no verified email or unresolved predicate; may be 'needs_review:<fields>')"}, 'person_first': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Person First', 'default': None, 'description': "named person's first name (person-target campaigns)"}, 'tech_receipt': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Tech Receipt', 'default': None, 'description': 'quotable proof line for the detected platform (dated evidence from the crawl) — forward it as-is'}, 'niche_receipt': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Niche Receipt', 'default': None, '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"}, 'review_status': {'enum': ['kept', 'discarded', 'unreviewed'], 'type': 'string', 'title': 'Review Status'}, 'last_upload_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Last Upload At', 'default': None, '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"}, 'person_receipt': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Person Receipt', 'default': None, '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'}, 'audience_receipt': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Audience Receipt', 'default': None, '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"}, 'discovery_source': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Discovery Source', 'default': None, 'description': 'which feed surfaced the lead (producthunt / yc / tinylaunch); empty for Maps and web-search leads'}, 'rejection_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Rejection Reason', 'default': None, '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"}, 'subscriber_count': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Subscriber Count', 'default': None, '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"}, 'verifier_verdict': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Verifier Verdict', 'default': None, 'description': "verifier's raw verdict (e.g. 'valid')"}}, 'additionalProperties': True}, 'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}, 'CampaignSummary': {'type': 'object', 'title': 'CampaignSummary', 'properties': {'sourced': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Sourced', 'default': None}, 'needs_review': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Needs Review', 'default': None}, 'fake_pipeline': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'title': 'Fake Pipeline', 'default': None, '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.'}, 'gate_survivors': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Gate Survivors', 'default': None, 'description': 'internal diagnostic'}, 'schema_version': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Schema Version', 'default': None, 'description': 'internal diagnostic'}, 'tier_a_matched': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Tier A Matched', 'default': None, 'description': 'internal diagnostic'}, 'flagged_reasons': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'integer'}}, {'type': 'null'}], 'title': 'Flagged Reasons', 'default': None}, 'removed_reasons': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'integer'}}, {'type': 'null'}], 'title': 'Removed Reasons', 'default': None}, 'total_discovered': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Total Discovered', 'default': None}}, 'additionalProperties': True}, 'FetchResultsResult': {'type': 'object', 'title': 'FetchResultsResult', 'required': ['total', 'offset', 'limit', 'credits_spent', 'rows'], 'properties': {'rows': {'type': 'array', 'items': {'$ref': '#/$defs/LeadRow'}, 'title': 'Rows'}, 'limit': {'type': 'integer', 'title': 'Limit'}, 'total': {'type': 'integer', 'title': 'Total'}, 'offset': {'type': 'integer', 'title': 'Offset'}, 'summary': {'anyOf': [{'$ref': '#/$defs/CampaignSummary'}, {'type': 'null'}], 'default': None}, 'credits_spent': {'type': 'integer', 'title': 'Credits Spent'}}}}, 'anyOf': [{'$ref': '#/$defs/FetchResultsResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'FetchResultsOutput'}
get_campaign_status
Get campaign status
Check the status of a previously submitted campaign (queued/running/done/ done_partial/failed) by job_id. Prefer wait_seconds=45 over sleeping between polls: the call holds until the status or pipeline stage changes (or the timer expires) and returns the normal snapshot either way — loop on it until `status` is terminal. Out-of-range wait_seconds clamps to [0, 45], never errors.
읽기 전용
입력 스키마
{'type': 'object', 'title': 'get_campaign_statusArguments', 'required': ['job_id'], 'properties': {'job_id': {'type': 'string', 'title': 'Job Id'}, 'wait_seconds': {'type': 'integer', 'title': 'Wait Seconds', 'default': 0}}}
출력 스키마
{'$defs': {'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}, 'CampaignSummary': {'type': 'object', 'title': 'CampaignSummary', 'properties': {'sourced': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Sourced', 'default': None}, 'needs_review': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Needs Review', 'default': None}, 'fake_pipeline': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'title': 'Fake Pipeline', 'default': None, '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.'}, 'gate_survivors': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Gate Survivors', 'default': None, 'description': 'internal diagnostic'}, 'schema_version': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Schema Version', 'default': None, 'description': 'internal diagnostic'}, 'tier_a_matched': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Tier A Matched', 'default': None, 'description': 'internal diagnostic'}, 'flagged_reasons': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'integer'}}, {'type': 'null'}], 'title': 'Flagged Reasons', 'default': None}, 'removed_reasons': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'integer'}}, {'type': 'null'}], 'title': 'Removed Reasons', 'default': None}, 'total_discovered': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Total Discovered', 'default': None}}, 'additionalProperties': True}, 'CampaignStatusResult': {'type': 'object', 'title': 'CampaignStatusResult', 'required': ['status', 'credits_spent'], 'properties': {'status': {'enum': ['queued', 'running', 'done', 'done_partial', 'failed'], 'type': 'string', 'title': 'Status', 'description': 'done_partial = finished but stopped early — status_reason says why; delivered results are still valid. failed = no results; status_reason says why.'}, 'summary': {'anyOf': [{'$ref': '#/$defs/CampaignSummary'}, {'type': 'null'}], 'default': None}, 'credits_spent': {'type': 'integer', 'title': '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)'}, 'status_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Status Reason', 'default': None, '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.'}}, 'additionalProperties': True}}, 'anyOf': [{'$ref': '#/$defs/CampaignStatusResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'CampaignStatusOutput'}
get_credits
Get credit balance
Check this tenant's prepaid credit balance (a validated lead costs 1 credit — 2 for person-target or Meta-ads campaigns), the available credit packs, and the ledger (paginate ledger entries with offset/limit, newest first). Campaigns cannot start with a zero balance — buy a pack via the dashboard when balance runs low. When you authenticate with an API key this also returns YOUR key's remaining budgets — check it before large campaigns.
읽기 전용
입력 스키마
{'type': 'object', 'title': 'get_creditsArguments', 'properties': {'limit': {'type': 'integer', 'title': 'Limit', 'default': 50}, 'offset': {'type': 'integer', 'title': 'Offset', 'default': 0}}}
출력 스키마
{'$defs': {'CreditsResult': {'type': 'object', 'title': 'CreditsResult', 'required': ['balance', 'packs', 'ledger'], 'properties': {'packs': {'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}, 'title': 'Packs'}, 'ledger': {'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}, 'title': 'Ledger'}, 'balance': {'type': 'integer', 'title': 'Balance'}}, 'additionalProperties': True}, 'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}}, 'anyOf': [{'$ref': '#/$defs/CreditsResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'CreditsOutput'}
get_icp_pack
List ICP packs
List the ICP packs (saved vertical/geo templates) available to this tenant.
읽기 전용
입력 스키마
{'type': 'object', 'title': 'get_icp_packArguments', 'properties': {}}
출력 스키마
{'$defs': {'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}, 'IcpPackResult': {'type': 'object', 'title': 'IcpPackResult', 'required': ['packs'], 'properties': {'packs': {'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}, 'title': 'Packs', 'description': 'saved vertical/geo templates; empty list is normal for a tenant with none — not an error'}}}}, 'anyOf': [{'$ref': '#/$defs/IcpPackResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'IcpPackOutput'}
get_usage
Get today's usage
Today's external vendor usage against the tenant's daily quota (vendor_calls_today / per_day_max / remaining). This is the OTHER ceiling besides credits: when remaining hits 0, campaign work pauses until midnight even with a healthy credit balance — check it before launching a large or urgent campaign. `remaining` is quota UNITS, which usually equals calls; a creator discovery costs one unit per delivered record, so it can consume far more than one call's worth. Mirrors REST GET /v1/usage.
읽기 전용
입력 스키마
{'type': 'object', 'title': 'get_usageArguments', 'properties': {}}
출력 스키마
{'$defs': {'UsageResult': {'type': 'object', 'title': 'UsageResult', 'required': ['vendor_calls_today', 'per_day_max', 'remaining'], 'properties': {'remaining': {'type': 'integer', 'title': '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'}, 'per_day_max': {'type': 'integer', 'title': 'Per Day Max', 'description': "the tenant's daily vendor quota, in units"}, 'vendor_calls_today': {'type': 'integer', 'title': 'Vendor Calls Today', 'description': 'external vendor calls (discovery, crawl, verification, enrichment) made for this tenant since midnight'}}}, 'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}}, 'anyOf': [{'$ref': '#/$defs/UsageResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'UsageOutput'}
list_campaigns
List campaigns
List this tenant's campaigns, newest first — the recovery path when a job_id was lost: every row carries job_id, status, the query it ran, and credits spent, so you can resume polling (get_campaign_status) or fetch results without resubmitting (and double-charging). Filters: q (substring over vertical/geo), status (queued | running | done | done_partial | failed), created_after/ created_before (ISO-8601 window over creation time), order ('desc' default, 'asc' oldest first). Paginate with offset/limit (limit 1-200); `total` counts everything matching the filters, not just this page.
읽기 전용
입력 스키마
{'type': 'object', 'title': 'list_campaignsArguments', 'properties': {'q': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Q', 'default': None}, 'limit': {'type': 'integer', 'title': 'Limit', 'default': 50}, 'order': {'type': 'string', 'title': 'Order', 'default': 'desc'}, 'offset': {'type': 'integer', 'title': 'Offset', 'default': 0}, 'status': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Status', 'default': None}, 'created_after': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Created After', 'default': None}, 'created_before': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Created Before', 'default': None}}}
출력 스키마
{'$defs': {'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}, 'CampaignSummary': {'type': 'object', 'title': 'CampaignSummary', 'properties': {'sourced': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Sourced', 'default': None}, 'needs_review': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Needs Review', 'default': None}, 'fake_pipeline': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'title': 'Fake Pipeline', 'default': None, '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.'}, 'gate_survivors': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Gate Survivors', 'default': None, 'description': 'internal diagnostic'}, 'schema_version': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Schema Version', 'default': None, 'description': 'internal diagnostic'}, 'tier_a_matched': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Tier A Matched', 'default': None, 'description': 'internal diagnostic'}, 'flagged_reasons': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'integer'}}, {'type': 'null'}], 'title': 'Flagged Reasons', 'default': None}, 'removed_reasons': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'integer'}}, {'type': 'null'}], 'title': 'Removed Reasons', 'default': None}, 'total_discovered': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Total Discovered', 'default': None}}, 'additionalProperties': True}, 'CampaignListItem': {'type': 'object', 'title': 'CampaignListItem', 'required': ['job_id', 'status'], 'properties': {'geo': {'type': 'string', 'title': 'Geo', 'default': ''}, 'query': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'title': 'Query', 'default': None, 'description': 'the structured query the campaign ran (including any `discovery` block) — reuse it to resubmit or refine'}, 'job_id': {'type': 'string', 'title': 'Job Id'}, 'status': {'enum': ['queued', 'running', 'done', 'done_partial', 'failed'], 'type': 'string', 'title': 'Status'}, 'summary': {'anyOf': [{'$ref': '#/$defs/CampaignSummary'}, {'type': 'null'}], 'default': None}, 'vertical': {'type': 'string', 'title': 'Vertical', 'default': '', 'description': 'empty for a locationless campaign (launch_feeds / web_search discovery)'}, 'created_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Created At', 'default': None}, 'agent_label': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Agent Label', 'default': None, 'description': 'label of the API key that submitted it; null for dashboard-submitted campaigns'}, 'finished_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Finished At', 'default': None}, 'credits_spent': {'type': 'integer', 'title': '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)'}, 'status_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Status Reason', 'default': None, 'description': 'why the campaign ended done_partial or failed; null otherwise'}}, 'additionalProperties': True}, 'ListCampaignsResult': {'type': 'object', 'title': 'ListCampaignsResult', 'required': ['total', 'offset', 'limit', 'campaigns'], 'properties': {'limit': {'type': 'integer', 'title': 'Limit'}, 'total': {'type': 'integer', 'title': 'Total', 'description': 'campaigns matching the filters (not just this page)'}, 'offset': {'type': 'integer', 'title': 'Offset'}, 'campaigns': {'type': 'array', 'items': {'$ref': '#/$defs/CampaignListItem'}, 'title': 'Campaigns'}}}}, 'anyOf': [{'$ref': '#/$defs/ListCampaignsResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'ListCampaignsOutput'}
review_lead
Review a lead
Mark a lead kept or discarded after review (status: kept | discarded | unreviewed). Then fetch_results(kept_only=true) returns only kept leads — the export set.
멱등성
입력 스키마
{'type': 'object', 'title': 'review_leadArguments', 'required': ['job_id', 'result_id', 'status'], 'properties': {'job_id': {'type': 'string', 'title': 'Job Id'}, 'status': {'type': 'string', 'title': 'Status'}, 'result_id': {'type': 'integer', 'title': 'Result Id'}}}
출력 스키마
{'$defs': {'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}, 'ReviewLeadResult': {'type': 'object', 'title': 'ReviewLeadResult', 'required': ['ok', 'job_id', 'result_id', 'status'], 'properties': {'ok': {'type': 'boolean', 'title': 'Ok'}, 'job_id': {'type': 'string', 'title': 'Job Id'}, 'status': {'enum': ['kept', 'discarded', 'unreviewed'], 'type': 'string', 'title': 'Status'}, 'result_id': {'type': 'integer', 'title': 'Result Id'}}}}, 'anyOf': [{'$ref': '#/$defs/ReviewLeadResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'ReviewLeadOutput'}
send_feedback
Send feedback
Send feedback to the Nose for Leads team. Set kind='product' for feedback about the leads, the service, coverage, or pricing — relay what the business owner tells you (categories: lead_quality, pricing, missing_feature, praise, other). Set kind='tool' (default) for feedback about these MCP tools themselves — a confusing description, a bug, a missing capability. Use freely; it's our main signal for improving the product and the agent experience.
입력 스키마
{'type': 'object', 'title': 'send_feedbackArguments', 'required': ['message'], 'properties': {'kind': {'enum': ['tool', 'product'], 'type': 'string', 'title': 'Kind', 'default': 'tool'}, 'tool': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Tool', 'default': None}, 'message': {'type': 'string', 'title': 'Message'}, 'category': {'enum': ['bug', 'confusing', 'missing_feature', 'lead_quality', 'pricing', 'praise', 'other'], 'type': 'string', 'title': 'Category', 'default': 'other'}}}
출력 스키마
{'$defs': {'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}, 'FeedbackResult': {'type': 'object', 'title': 'FeedbackResult', 'required': ['ok'], 'properties': {'ok': {'type': 'boolean', 'title': 'Ok'}}}}, 'anyOf': [{'$ref': '#/$defs/FeedbackResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'FeedbackOutput'}
start_campaign
Start lead campaign
Submit a lead-generation campaign. Pass translate_icp's `vertical`, `geo`, and `query` fields directly (category/location are ALIASES for vertical/geo, accepted so translate output round-trips; prefer vertical/geo when constructing calls). `query`'s full shape and field vocabulary are in this tool's schema — build it with translate_icp from free text (recommended), or construct it directly. An unknown predicate field (or rank), and any unknown `discovery` key, is rejected with a 422 naming it; an unknown top-level `query` key is silently ignored, so keep to predicates/rank/discovery — a typo there will not error, it will just do nothing. Costs 1 credit per validated lead — every campaign type, including founder/person campaigns and campaigns that filter on Meta ads. A campaign cannot overspend: if discovered leads exceed your balance it delivers what your credits cover and the campaign ends `done_partial`; check get_credits first if your balance is low. vertical/geo are REQUIRED except for a locationless campaign — `query` carrying a discovery block with mode 'launch_feeds' or 'web_search' — which has no geography at all. If the tenant has no card on file this returns a 403 `card_required` error with a `setup_url` — surface that URL to the user so they can verify a card (never charged unless they buy a pack). Pass a stable idempotency_key when you might retry — a retry returns the ORIGINAL campaign (idempotent_replay=true), never a duplicate charge. Returns {job_id, idempotent_replay}; poll get_campaign_status with job_id.
외부 접근 가능
입력 스키마
{'type': 'object', '$defs': {'QueryInput': {'type': 'object', 'title': 'QueryInput', 'properties': {'rank': {'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'], 'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Rank', 'default': None, 'description': 'optional field to sort/prioritize results by (same vocabulary as predicate fields)'}, 'discovery': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'title': 'Discovery', 'default': None, '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.'}, 'predicates': {'type': 'array', 'items': {'$ref': '#/$defs/QueryPredicateInput'}, 'title': 'Predicates', 'description': 'all predicates must hold for a lead to count as a confirmed match'}}, '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.", 'additionalProperties': True}, 'QueryPredicateInput': {'type': 'object', 'title': 'QueryPredicateInput', 'required': ['field', 'op'], 'properties': {'op': {'enum': ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'contains', 'not_contains', 'in', 'exists'], 'type': 'string', 'title': 'Op', 'description': 'comparison operator'}, 'field': {'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'], 'type': 'string', 'title': '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.", 'x-field-catalog': {'has_h1': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, '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)'}, 'rating': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq', 'neq'], 'type': 'float', 'example': 4.2, 'description': 'average star rating, 0-5'}, 'category': {'ops': ['eq'], 'type': 'str', 'example': 'plumber', 'description': 'the business vertical/type — goes in the top-level `category`, NOT a predicate'}, 'licensed': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': True, 'description': 'site states licensed/insured'}, 'platform': {'ops': ['eq', 'neq'], 'type': 'str', 'example': 'wix', 'description': 'site builder/platform (wix, squarespace, wordpress, godaddy, weebly, shopify, custom)'}, 'ads_state': {'ops': ['eq', 'neq', 'exists'], 'type': 'str', 'values': ['ADS_PRESENT', 'NO_ADS_FOUND'], 'example': 'NO_ADS_FOUND', 'description': 'paid-ads verdict ADS_PRESENT/NO_ADS_FOUND, null when unresolved'}, 'has_hours': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': True, 'description': 'business lists opening hours'}, 'has_title': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': True, 'description': 'the homepage has a non-empty <title>'}, 'serp_rank': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq'], 'type': 'int', 'example': 10, '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."}, 'has_robots': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, '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)'}, 'is_claimed': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, 'description': 'the Google listing is claimed/managed by the owner'}, 'page_count': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq', 'neq'], 'type': 'int', 'example': 1, 'description': 'how many of home/contact/about/team pages resolved'}, 'tech_state': {'ops': ['eq', 'neq', 'exists'], 'type': 'str', 'values': ['TECH_DETECTED', 'TECH_NONE_DETECTED'], 'example': 'TECH_DETECTED', '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'}, 'has_sitemap': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, '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)'}, 'has_website': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, 'description': 'business has any website at all'}, 'areas_served': {'ops': ['contains', 'not_contains'], 'type': 'list[str]', 'example': 'Phoenix', 'description': 'service-area city names parsed from the site'}, 'has_viewport': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, 'description': 'the site is mobile-friendly (has a viewport meta tag)'}, 'is_franchise': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, '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'}, 'review_count': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq', 'neq'], 'type': 'int', 'example': 30, 'description': 'number of reviews'}, 'total_photos': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq', 'neq'], 'type': 'int', 'example': 10, 'description': 'number of listing photos'}, 'booking_state': {'ops': ['eq', 'neq', 'exists'], 'type': 'str', 'values': ['BOOKING_PRESENT', 'BOOKING_ABSENT'], 'example': 'BOOKING_ABSENT', 'description': 'online-booking verdict BOOKING_PRESENT/BOOKING_ABSENT, null when unresolved'}, 'detected_tech': {'ops': ['contains', 'not_contains'], 'type': 'list[str]', 'example': 'shopify', 'description': 'tech/widgets detected on the site (e.g. shopify, calendly, toast, doordash)'}, 'has_real_site': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, 'description': 'has a real website (not social-only / not empty)'}, 'in_local_pack': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, 'description': 'appears in the Google Maps local 3-pack for its category+city'}, 'runs_meta_ads': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, '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'}, 'website_state': {'ops': ['eq', 'neq', 'exists'], 'type': 'str', 'values': ['WEBSITE_NONE', 'WEBSITE_SOCIAL_ONLY', 'WEBSITE_PRESENT'], 'example': 'WEBSITE_NONE', 'description': 'website-presence verdict — WEBSITE_NONE / WEBSITE_SOCIAL_ONLY / WEBSITE_PRESENT, null when unresolved'}, 'emergency_24_7': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': True, 'description': 'site advertises 24/7 emergency service'}, 'has_schema_org': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, 'description': 'the site has schema.org structured data'}, 'is_social_only': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': True, 'description': 'the only web presence is a social page (Facebook/Instagram/etc.)'}, 'multi_location': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, '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'}, 'outdated_state': {'ops': ['eq', 'neq', 'exists'], 'type': 'str', 'values': ['OUTDATED', 'CURRENT'], 'example': 'OUTDATED', 'description': 'outdated-website verdict OUTDATED/CURRENT, null when unresolved'}, 'first_review_at': {'ops': ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'exists'], 'type': 'str', 'example': '2024-01-15', '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"}, 'runs_google_ads': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, 'description': 'the business runs Google Search ads (advertiser-level, any campaign)'}, 'has_contact_form': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, 'description': 'the site has a contact/inquiry form'}, 'seo_basics_state': {'ops': ['eq', 'neq', 'exists'], 'type': 'str', 'values': ['SEO_BASICS_MISSING', 'SEO_BASICS_PRESENT'], 'example': 'SEO_BASICS_MISSING', '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"}, 'titles_identical': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': True, '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)'}, 'years_in_business': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq', 'neq'], 'type': 'int', 'example': 1998, '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"}, 'review_trend_state': {'ops': ['eq', 'neq', 'exists'], 'type': 'str', 'example': 'NEGATIVE_WAVE', '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"}, 'website_social_only': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': True, 'description': "the business's only web presence is a social/directory page like Facebook, no real website"}, 'has_meta_description': {'ops': ['eq', 'exists'], 'type': 'bool', 'example': False, 'description': 'the homepage has a meta description (basic SEO)'}, 'low_star_reviews_30d': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq', 'neq'], 'type': 'int', 'example': 3, '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"}, 'low_star_reviews_60d': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq', 'neq'], 'type': 'int', 'example': 3, '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"}, 'low_star_reviews_90d': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq', 'neq'], 'type': 'int', 'example': 3, '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"}, 'locations_in_campaign': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq', 'neq'], 'type': 'int', 'example': 3, 'description': 'how many businesses in this search share this name or domain at different addresses (>1 = multi-location/chain in this run)'}, 'digital_maturity_score': {'ops': ['lt', 'lte', 'gt', 'gte', 'eq', 'neq'], 'type': 'float', 'example': 50, 'description': '0-100 composite web-maturity score (low = bigger digital gap)'}}}, 'value': {'title': 'Value', 'default': None, 'description': "comparison value; omit for op='exists'"}}, 'additionalProperties': True}}, 'title': 'start_campaignArguments', 'required': ['query'], 'properties': {'geo': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Geo', 'default': None}, 'query': {'$ref': '#/$defs/QueryInput'}, 'category': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Category', 'default': None}, 'location': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Location', 'default': None}, 'vertical': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Vertical', 'default': None}, 'idempotency_key': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Idempotency Key', 'default': None}}}
출력 스키마
{'$defs': {'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}, 'StartCampaignResult': {'type': 'object', 'title': 'StartCampaignResult', 'required': ['job_id'], 'properties': {'job_id': {'type': 'string', 'title': 'Job Id'}, 'idempotent_replay': {'type': 'boolean', 'title': 'Idempotent Replay', 'default': False}}}}, 'anyOf': [{'$ref': '#/$defs/StartCampaignResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'StartCampaignOutput'}
translate_icp
Translate ICP to query
Translate free-text ICP description (e.g. "plumbers in phoenix with no website") into a structured query. Call this BEFORE start_campaign — start_campaign only accepts a structured `query` dict, never free text. Returns {query, vertical, geo, category, location, preview, unsupported, discovery} — pass vertical/geo/query straight to start_campaign. `discovery` is non-empty when the ask routes OUTSIDE Google Maps, in one of two shapes. Mode 'launch_feeds': people/companies that RECENTLY LAUNCHED (e.g. "founders of B2B SaaS that launched in the last 3 months") — it names the launch feeds to search and whether the leads are people or companies. Mode 'web_search': long-tail ONLINE businesses Google Maps never had (e.g. "marketing agencies for dentists"), found via paid organic search — companies only, never people. Either way the block is already embedded inside the returned `query`, so passing `query` through unchanged is all that's needed; neither mode has a geography, so vertical/geo come back empty and start_campaign does not require them. Anything we cannot actually source lands in `unsupported` instead — read that list aloud to the user rather than pretending the campaign will cover it.
읽기 전용
입력 스키마
{'type': 'object', 'title': 'translate_icpArguments', 'required': ['text'], 'properties': {'text': {'type': 'string', 'title': 'Text'}}}
출력 스키마
{'$defs': {'ErrorEnvelope': {'type': 'object', 'title': 'ErrorEnvelope', 'required': ['code', 'message'], 'properties': {'code': {'type': 'string', 'title': 'Code'}, 'message': {'type': 'string', 'title': 'Message'}}, '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.', 'additionalProperties': True}, 'TranslateResult': {'type': 'object', 'title': 'TranslateResult', 'required': ['query', 'vertical', 'geo', 'category', 'location', 'preview', 'unsupported'], 'properties': {'geo': {'type': 'string', 'title': 'Geo', 'description': 'Pass to start_campaign as geo'}, 'query': {'type': 'object', 'title': 'Query', 'additionalProperties': True}, 'preview': {'type': 'string', 'title': 'Preview'}, 'category': {'type': 'string', 'title': 'Category'}, 'location': {'type': 'string', 'title': 'Location'}, 'vertical': {'type': 'string', 'title': 'Vertical', 'description': 'Pass to start_campaign as vertical'}, 'discovery': {'type': 'object', 'title': 'Discovery', '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.", 'additionalProperties': True}, 'unsupported': {'type': 'array', 'items': {'type': 'string'}, 'title': 'Unsupported'}, 'suggested_rewrite': {'type': 'string', 'title': '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.'}}, 'additionalProperties': True}}, 'anyOf': [{'$ref': '#/$defs/TranslateResult'}, {'$ref': '#/$defs/ErrorEnvelope'}], 'title': 'TranslateOutput'}
추가됨
send_feedback
2026년 9월 17일 12:36 PM
추가됨
get_usage
2026년 9월 17일 12:36 PM
추가됨
list_campaigns
2026년 9월 17일 12:36 PM
추가됨
get_credits
2026년 9월 17일 12:36 PM
추가됨
review_lead
2026년 9월 17일 12:36 PM
추가됨
add_suppression
2026년 9월 17일 12:36 PM
추가됨
get_icp_pack
2026년 9월 17일 12:36 PM
추가됨
fetch_results
2026년 9월 17일 12:36 PM
추가됨
get_campaign_status
2026년 9월 17일 12:36 PM
추가됨
start_campaign
2026년 9월 17일 12:36 PM
추가됨
translate_icp
2026년 9월 17일 12:36 PM