scrapeloop-mcp 0.37.5 → 0.38.0
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/package.json +1 -1
- package/src/index.js +59 -1
- package/src/mutation-policies.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrapeloop-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
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
|
@@ -638,7 +638,7 @@ const TOOLS = {
|
|
|
638
638
|
run: ({ plan_id, ...body }) => api('PATCH', `/plans/${enc(plan_id)}`, body),
|
|
639
639
|
},
|
|
640
640
|
check_campaign_setup: {
|
|
641
|
-
def: { description: 'One free read that walks a whole self-filling campaign and says whether its parts fit each other: the search plan, the Tables and rules on the way, the checks, the email answer (Table rule, plan goal and campaign must agree), the campaign, the credit limits and the approval. Pass plan_id for a search plan, or campaign_id to start from a campaign (works with no plan too). Returns fits (true only when nothing is fail and nothing is unknown), counts, and checks in the order a lead travels; each has step, key, status (ok, warn, fail or unknown), a plain title and detail, and often fixes: the exact tool and args that end it. Run it after every build step and before asking for a start. Apply a fix only as its note says, ask the person before any fix that changes who gets an email or what may be spent, and run this again afterwards. It changes nothing, starts nothing and approves nothing. deep=false skips the sender read.', inputSchema: obj({
|
|
641
|
+
def: { description: 'One free read that walks a whole self-filling campaign and says whether its parts fit each other: the search plan, the Tables and rules on the way, the checks, the email answer (Table rule, plan goal and campaign must agree), the campaign, the credit limits and the approval. Pass plan_id for a search plan, or campaign_id to start from a campaign (works with no plan too). Returns fits (true only when nothing is fail and nothing is unknown), counts, and checks in the order a lead travels; each has step, key, status (ok, warn, fail or unknown), a plain title and detail, and often fixes: the exact tool and args that end it. Run it after every build step and before asking for a start. Apply a fix only as its note says, ask the person before any fix that changes who gets an email or what may be spent, and run this again afterwards. It changes nothing, starts nothing and approves nothing. deep=false is the quick check: it skips the sender read, the Start check of the campaign and the count of leads that meet a rule and have not moved, so it can say fits while one of those three would not. Use the full check before a start.', inputSchema: obj({
|
|
642
642
|
plan_id: { ...S, format: 'uuid' }, campaign_id: { ...S, format: 'uuid' }, deep: B,
|
|
643
643
|
}) },
|
|
644
644
|
run: (a) => {
|
|
@@ -2419,6 +2419,64 @@ const TOOLS = {
|
|
|
2419
2419
|
});
|
|
2420
2420
|
},
|
|
2421
2421
|
},
|
|
2422
|
+
run_table_flow_rule_for_waiting_rows: {
|
|
2423
|
+
def: {
|
|
2424
|
+
description:
|
|
2425
|
+
'Run one saved Table rule for the rows that meet it right now and have not moved. A rule only runs when something happens to a lead, so leads can meet a rule and stay where they are: the rule was changed after they arrived, or a check finished and no rule looked again. check_campaign_setup names this tool under "Leads meet a rule and have not moved". It only works for a rule that sends leads to another Table, that is switched on and not paused, on a normal Table of 5,000 rows or fewer. A rule that asks for a saved review count only on a Table of 100 rows or fewer. With dry_run true (the default) it only reads. It returns count (the leads that meet the rule now, have not gone on and have nothing waiting to handle them), would_queue, sends_to (where the rule sends them, with the paid checks that Table runs on new leads by itself and the credits a lead), plan (the search plan these Tables belong to), and a title and lines ready to show the person. To run it, call it again with dry_run false and expected_count set to that count. If the count changed in between, nothing is put in line and the new count comes back: show a fresh dry run. A real run moves nothing itself. It puts the leads in line for this one rule, at most 1,000 a call (left_waiting says how many are left, so call again when those are handled). The rule then looks at each lead again when its turn comes, about 2 seconds a lead, so a lead that no longer meets the rule stays where it is. The loop guard and the daily limit of the rule apply as always, and a rule with a wait keeps its wait. Always show the dry run lines first and run only after the person says yes: a lead that moves on can start a paid check in the next Table. On a Table of a running search plan the plan starts no new search until these leads are handled, and this tool never pauses or starts a plan. Under a hard monthly limit the campaign only takes leads that the searches of that plan found, so never promise that a moved lead goes into the campaign. One operation_id is reused across retries, so a lost reply never puts a lead in line twice.',
|
|
2426
|
+
inputSchema: obj(
|
|
2427
|
+
{
|
|
2428
|
+
list_id: { ...S, format: 'uuid', description: 'The Table the rule is on.' },
|
|
2429
|
+
rule_id: { ...S, format: 'uuid' },
|
|
2430
|
+
dry_run: { ...B, description: 'True (the default) only reads. Only a real false runs the rule.' },
|
|
2431
|
+
expected_count: {
|
|
2432
|
+
type: 'integer',
|
|
2433
|
+
minimum: 0,
|
|
2434
|
+
description: 'The count of the dry run the person was shown. Needed with dry_run false.',
|
|
2435
|
+
},
|
|
2436
|
+
operation_id: { ...S, format: 'uuid', description: 'Optional retry key from a prior ambiguous call.' },
|
|
2437
|
+
},
|
|
2438
|
+
['list_id', 'rule_id'],
|
|
2439
|
+
),
|
|
2440
|
+
},
|
|
2441
|
+
run: (a) => {
|
|
2442
|
+
if (!a.list_id || !a.rule_id) {
|
|
2443
|
+
return { ok: false, status: 400, error: { detail: 'list_id and rule_id are required.' } };
|
|
2444
|
+
}
|
|
2445
|
+
const dryRun = a.dry_run !== false;
|
|
2446
|
+
if (!dryRun && !Number.isInteger(a.expected_count)) {
|
|
2447
|
+
return {
|
|
2448
|
+
ok: false,
|
|
2449
|
+
status: 400,
|
|
2450
|
+
error: { detail: 'Run a dry run first, then pass its count as expected_count with dry_run false.' },
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
const requestBody = dryRun
|
|
2454
|
+
? { dry_run: true }
|
|
2455
|
+
: { dry_run: false, expected_count: a.expected_count, operation_id: a.operation_id || randomUUID() };
|
|
2456
|
+
// The read behind it is as heavy as one count of the setup check. One
|
|
2457
|
+
// more try is enough: three would only load the database three times.
|
|
2458
|
+
return api('POST', `/lists/${enc(a.list_id)}/flow-rules/${enc(a.rule_id)}/run-waiting`, requestBody, {
|
|
2459
|
+
maxRetries: 1,
|
|
2460
|
+
...(dryRun
|
|
2461
|
+
? {}
|
|
2462
|
+
: {
|
|
2463
|
+
reconciliation: {
|
|
2464
|
+
inspect_with: {
|
|
2465
|
+
tool: 'run_table_flow_rule_for_waiting_rows',
|
|
2466
|
+
arguments: { list_id: a.list_id, rule_id: a.rule_id, dry_run: true },
|
|
2467
|
+
},
|
|
2468
|
+
operation_id: requestBody.operation_id,
|
|
2469
|
+
retry_with: {
|
|
2470
|
+
tool: 'run_table_flow_rule_for_waiting_rows',
|
|
2471
|
+
arguments: { ...a, operation_id: requestBody.operation_id },
|
|
2472
|
+
},
|
|
2473
|
+
retry_guidance:
|
|
2474
|
+
`Retry run_table_flow_rule_for_waiting_rows with the same operation_id ${requestBody.operation_id}. Never use a new one for this run. A dry run shows what still waits: a lead that is already in line is no longer counted.`,
|
|
2475
|
+
},
|
|
2476
|
+
}),
|
|
2477
|
+
});
|
|
2478
|
+
},
|
|
2479
|
+
},
|
|
2422
2480
|
get_flow_rule_stats: {
|
|
2423
2481
|
def: {
|
|
2424
2482
|
description:
|
package/src/mutation-policies.js
CHANGED
|
@@ -206,6 +206,15 @@ export const MUTATION_POLICIES = Object.freeze({
|
|
|
206
206
|
['get_flow_rule_stats', 'list_table_flow_rules', 'list_campaign_flow_rules'],
|
|
207
207
|
['list_id', 'campaign_id', 'rule_id', 'lead_id', 'execute'],
|
|
208
208
|
),
|
|
209
|
+
// A dry run reads. A real run carries one operation_id: sent again it
|
|
210
|
+
// answers with the first result and puts no lead in line a second time
|
|
211
|
+
// (tests/test_flow_rule_run_waiting_db.py), so a repeat after a lost reply
|
|
212
|
+
// is safe.
|
|
213
|
+
run_table_flow_rule_for_waiting_rows: idempotent(
|
|
214
|
+
'POST',
|
|
215
|
+
['list_table_flow_rules', 'get_flow_rule_stats'],
|
|
216
|
+
['list_id', 'rule_id', 'dry_run', 'expected_count', 'operation_id'],
|
|
217
|
+
),
|
|
209
218
|
preview_send_to_table: read(),
|
|
210
219
|
send_to_table: idempotent('POST', ['get_list_rows'], ['list_id', 'destination_list_id', 'new_table_name']),
|
|
211
220
|
preview_table_write_back: read(),
|