scrapeloop-mcp 0.37.4 → 0.37.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,8 +81,12 @@ LinkedIn, then external ID or source URL, all scoped to the API key's workspace.
81
81
  A later import with a real email fills in the lead that had none.
82
82
  A row that cannot be imported is skipped and comes back in `invalid_reasons`.
83
83
  Each entry has a `reason` written for the user: show it as it is written.
84
- One exception: a preview of sourcing rows can answer short codes joined by a
85
- semicolon. Put those in your own plain words.
84
+ Records from a public source list can only be previewed. An import is read
85
+ that way when a record has `source`, `source_state` and `niche` together, when
86
+ a CSV has those three columns, or when `sourcing_contract` is set. It is then
87
+ refused as a whole. The answer says what was seen and what to change, and a
88
+ preview of such a batch carries a `warning` that says the same. For your own
89
+ leads leave `source_state` out and use `state_region`.
86
90
  Mutating imports generate one `operation_id` and reuse it across transient HTTP
87
91
  retries. The API returns the first committed result for the same key and
88
92
  payload, and rejects reuse with different inputs. If every response is lost,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrapeloop-mcp",
3
- "version": "0.37.4",
3
+ "version": "0.37.5",
4
4
  "description": "Scrapeloop MCP server - set up and run Scrapeloop end-to-end (integrations, scraping, lead database, verification, cleaners, enrichment, strategies, Instantly campaigns) from any MCP client (Claude Desktop, Cursor, ChatGPT, …).",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -1863,7 +1863,7 @@ const TOOLS = {
1863
1863
  import_leads: {
1864
1864
  def: {
1865
1865
  description:
1866
- 'Bring your own leads: atomically import externally-sourced records into a campaign-bindable Scrapeloop Table without scraping or lead credits. A generic record needs at least one of: email, linkedin_url, external_id (your own id for the record) or an absolute source_url (the page the record came from), so Scrapeloop can tell which lead it is. Email may be null. Records from a public source list cannot be imported here, only previewed. The rules for such records (sourcing_contract) switch on when you pass sourcing_contract:true, and also by themselves when one record carries source, source_state and niche together. A real import is then refused with 409 and nothing is imported: no lead, no Table. The 409 has an error sentence written for the user, destination "agency_local_businesses" and dry_run_available:true. Use preview_import to see what such records would do. In that preview each record needs a controlled source, source_state and niche and a valid phone or email, a local business with only a phone gets an id made from its source and its phone, and source and niche tags are worked out. If these are the user\'s own leads, say what happened and ask before you change them: without source_state the same records import as ordinary leads. Matching is workspace-scoped and deterministic: normalized email first, then local-business phone, canonical LinkedIn, then external_id or source_url. The legacy dedupe_by="external_id" mode matches by external_id or source_url first. A later record with a real email merges into the earlier lead that only had a LinkedIn link, an external_id or a source_url, instead of duplicating it. No placeholder emails are created. verification_status and custom_fields are preserved, and custom_fields flow to Instantly as custom variables. Target list_id or pass list_name to create-or-get a Table. Batch up to 1000 records. One operation_id is reused across HTTP retries, so an uncertain response can be recovered without duplicating leads. You may pass a prior operation_id; reusing it with different inputs returns 409. Each record: {source?, source_state?, email?, linkedin_url?, source_url?, external_id?, first_name, last_name, business_name, phone, website, address, city, state_region, country, niche, rating, reviews_count, gbp_link?, facebook?, instagram?, verification_status ("verified"|"catchall_verified"|"unverified"|"risky"|"invalid"), strategy_tags:[], custom_fields:{}}. Returns {operation_id, list_id, inserted, updated, deduped, invalid, invalid_reasons}. Each entry of invalid_reasons has a reason written for the user: show it as it is written.',
1866
+ 'Bring your own leads: atomically import externally-sourced records into a campaign-bindable Scrapeloop Table without scraping or lead credits. A generic record needs at least one of: email, linkedin_url, external_id (your own id for the record) or an absolute source_url (the page the record came from), so Scrapeloop can tell which lead it is. Email may be null. Records from a public source list cannot be imported here, only previewed. The rules for such records (sourcing_contract) switch on when you pass sourcing_contract:true, and also by themselves when one record carries source, source_state and niche together. A real import is then refused with 409 and nothing is imported: no lead, no Table. The 409 has an error sentence written for the user (it says what was seen, such as how many records carry all three, and what to change: show it as it is written), destination "agency_local_businesses" and dry_run_available:true. Use preview_import to see what such records would do. That preview has a warning that says the same. A reason of a skipped record there ends with: fixing this will not get it imported. Follow the warning, do not try to make such records pass. In that preview each record needs a controlled source, source_state and niche and a valid phone or email, a local business with only a phone gets an id made from its source and its phone, and source and niche tags are worked out. If these are the user\'s own leads, say what happened and ask before you change them: without source_state the same records import as ordinary leads. Matching is workspace-scoped and deterministic: normalized email first, then local-business phone, canonical LinkedIn, then external_id or source_url. The legacy dedupe_by="external_id" mode matches by external_id or source_url first. A later record with a real email merges into the earlier lead that only had a LinkedIn link, an external_id or a source_url, instead of duplicating it. No placeholder emails are created. verification_status and custom_fields are preserved, and custom_fields flow to Instantly as custom variables. Target list_id or pass list_name to create-or-get a Table. Batch up to 1000 records. One operation_id is reused across HTTP retries, so an uncertain response can be recovered without duplicating leads. You may pass a prior operation_id; reusing it with different inputs returns 409. Each record: {source?, source_state?, email?, linkedin_url?, source_url?, external_id?, first_name, last_name, business_name, phone, website, address, city, state_region, country, niche, rating, reviews_count, gbp_link?, facebook?, instagram?, verification_status ("verified"|"catchall_verified"|"unverified"|"risky"|"invalid"), strategy_tags:[], custom_fields:{}}. Returns {operation_id, list_id, inserted, updated, deduped, invalid, invalid_reasons}. Each entry of invalid_reasons has a reason written for the user: show it as it is written.',
1867
1867
  inputSchema: obj(
1868
1868
  {
1869
1869
  list_id: S,
@@ -2061,7 +2061,7 @@ const TOOLS = {
2061
2061
  import_leads_csv: {
2062
2062
  def: {
2063
2063
  description:
2064
- "Bring your own leads from CSV/TSV into a campaign-bindable Table. Each row needs at least one of: email, LinkedIn URL, external ID or an absolute source URL, so Scrapeloop can tell which lead it is. A row with no email needs no placeholder email. The parser auto-detects the delimiter, maps common headers (including linkedin_url and source_url), preserves unmapped columns as custom_fields, and normalizes verifier verdicts. A CSV from a public source list cannot be imported here, only previewed. A file with source, source_state and niche columns switches on the rules for such lists (sourcing_contract) by itself, and so does sourcing_contract:true. A real import of it is refused with 409 and nothing is imported: no lead, no Table. The 409 has an error sentence written for the user, destination \"agency_local_businesses\" and dry_run_available:true. With dry_run:true the same file is previewed: every loaded row must also have a valid phone or email, source IDs are namespaced, a local business with only a phone gets an id made from its source and its phone, and source/niche tags are worked out. If this is the user's own file, say what happened and ask before you change it: without the source_state column the same rows import as ordinary leads. Matching is workspace-scoped: email first, then local-business phone, LinkedIn, then external ID or source URL. Set dry_run:true first to preview writes and column mapping. Mutating CSV imports are atomic and reuse one operation_id across HTTP retries. You may pass a prior operation_id; reusing it with changed CSV inputs returns 409. No scraping, lead credits, or re-verification. Target list_id or pass list_name. Up to 1000 rows per call. Skipped rows come back in invalid_reasons. Each entry has a reason written for the user: show it as it is written. One exception: in a dry run with the sourcing contract on, a reason can be short codes joined by a semicolon. Put those in your own plain words.",
2064
+ "Bring your own leads from CSV/TSV into a campaign-bindable Table. Each row needs at least one of: email, LinkedIn URL, external ID or an absolute source URL, so Scrapeloop can tell which lead it is. A row with no email needs no placeholder email. The parser auto-detects the delimiter, maps common headers (including linkedin_url and source_url), preserves unmapped columns as custom_fields, and normalizes verifier verdicts. A CSV from a public source list cannot be imported here, only previewed. A file with source, source_state and niche columns switches on the rules for such lists (sourcing_contract) by itself, and so does sourcing_contract:true. A real import of it is refused with 409 and nothing is imported: no lead, no Table. The 409 has an error sentence written for the user (it says what was seen, such as the names of the three columns, and what to change: show it as it is written), destination \"agency_local_businesses\" and dry_run_available:true. With dry_run:true the same file is previewed (the answer then has a warning that says the real import will be refused, and what to change): every loaded row must also have a valid phone or email, source IDs are namespaced, a local business with only a phone gets an id made from its source and its phone, and source/niche tags are worked out. If this is the user's own file, say what happened and ask before you change it: without the source_state column the same rows import as ordinary leads. With a name of its own, such as Lead state, that column is kept as an extra field. Matching is workspace-scoped: email first, then local-business phone, LinkedIn, then external ID or source URL. Set dry_run:true first to preview writes and column mapping. Mutating CSV imports are atomic and reuse one operation_id across HTTP retries. You may pass a prior operation_id; reusing it with changed CSV inputs returns 409. No scraping, lead credits, or re-verification. Target list_id or pass list_name. Up to 1000 rows per call. Skipped rows come back in invalid_reasons. Each entry has a reason written for the user: show it as it is written.",
2065
2065
  inputSchema: obj(
2066
2066
  {
2067
2067
  csv: S,
@@ -2117,7 +2117,7 @@ const TOOLS = {
2117
2117
  preview_import: {
2118
2118
  def: {
2119
2119
  description:
2120
- 'Dry-run for import_leads: report exactly what an import WOULD do ({would_insert, would_update, deduped, invalid, invalid_reasons, list_exists}) WITHOUT writing anything (no leads, no list, no tags created). Same inputs as import_leads. Call this first for a bring-your-own batch, show the user the plan (e.g. "adds 340 new, updates 12, skips 3 bad rows"), then confirm before import_leads. One kind of batch can only be previewed: records under the rules for public source lists (sourcing_contract:true, or a record with source, source_state and niche together). The preview works for them, and a real import is refused with 409. Each entry of invalid_reasons has a reason written for the user: show it as it is written. One exception: with sourcing_contract on, a reason can be short codes joined by a semicolon. Put those in your own plain words.',
2120
+ 'Dry-run for import_leads: report exactly what an import WOULD do ({would_insert, would_update, deduped, invalid, invalid_reasons, list_exists}) WITHOUT writing anything (no leads, no list, no tags created). Same inputs as import_leads. Call this first for a bring-your-own batch, show the user the plan (e.g. "adds 340 new, updates 12, skips 3 bad rows"), then confirm before import_leads. One kind of batch can only be previewed: records under the rules for public source lists (sourcing_contract:true, or a record with source, source_state and niche together). The preview works for them, and a real import is refused with 409. The preview then has a warning that says so and what to change: show it as it is written, and follow it. Fixing the skipped records of such a batch does not get them imported. Each entry of invalid_reasons has a reason written for the user: show it as it is written.',
2121
2121
  inputSchema: obj(
2122
2122
  {
2123
2123
  list_id: S,