@kaminari-ad/mcp 0.9.1 → 0.11.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { err, ok } from './chunk-JEGBPX3R.js';
2
+ import { err, ok } from './chunk-XSI6SZTX.js';
3
3
  import { createHash, randomUUID } from 'crypto';
4
4
  import createClient from 'openapi-fetch';
5
5
  import { fetch } from 'undici';
@@ -240,17 +240,22 @@ var ProxyTargetRequest = z.object({
240
240
  city: z.string().default(""),
241
241
  isp: z.string().default("")
242
242
  }).partial().passthrough();
243
+ var RepeatModeType = z.enum(["isolated", "shared"]);
243
244
  z.object({
244
245
  url: z.union([z.string(), z.null()]).optional(),
245
246
  ad_tag: z.union([z.string(), z.null()]).optional(),
246
247
  vast_tag: z.union([z.string(), z.null()]).optional(),
248
+ referrer: z.union([z.string(), z.null()]).optional(),
247
249
  country_code: z.string().min(2).max(2),
248
250
  emulator_id: z.string().min(1).max(100),
249
251
  proxy: ProxyTargetRequest.optional(),
250
252
  labels: z.record(z.string()).optional(),
251
253
  campaign_id: z.union([z.string(), z.null()]).optional(),
252
254
  run_id: z.union([z.string(), z.null()]).optional(),
253
- ad_discovery: z.boolean().optional().default(false)
255
+ ad_discovery: z.boolean().optional().default(false),
256
+ repeat_count: z.number().int().gte(1).optional().default(1),
257
+ repeat_mode: RepeatModeType.optional(),
258
+ retry_max_attempts: z.number().int().gte(0).optional().default(0)
254
259
  }).passthrough();
255
260
  var ScanStatus = z.enum([
256
261
  "pending",
@@ -290,7 +295,8 @@ var VideoMetaResponse = z.object({
290
295
  vast_version: z.string().default(""),
291
296
  ad_system: z.string().default(""),
292
297
  is_vpaid: z.boolean().default(false),
293
- wrapper_depth: z.number().int().default(0)
298
+ wrapper_depth: z.number().int().default(0),
299
+ click_through: z.string().default("")
294
300
  }).partial().passthrough();
295
301
  var ProxyTargetResponse = z.object({
296
302
  proxy_type: z.string(),
@@ -357,8 +363,12 @@ var ScanResponse = z.object({
357
363
  public_report_url: z.string().optional().default(""),
358
364
  ad_tag: z.union([z.string(), z.null()]).optional(),
359
365
  vast_tag: z.union([z.string(), z.null()]).optional(),
360
- creative_kind: z.string().optional().default("banner"),
366
+ referrer: z.union([z.string(), z.null()]).optional(),
367
+ creative_kind: z.enum(["banner", "video"]).optional().default("banner"),
361
368
  creative_screenshot_url: z.string().optional().default(""),
369
+ creative_video_url: z.string().optional().default(""),
370
+ vast_xml_url: z.string().optional().default(""),
371
+ creative_html_url: z.string().optional().default(""),
362
372
  creative_width: z.number().int().optional().default(0),
363
373
  creative_height: z.number().int().optional().default(0),
364
374
  video: z.union([VideoMetaResponse, z.null()]).optional(),
@@ -374,7 +384,13 @@ var ScanResponse = z.object({
374
384
  campaign_group_name: z.union([z.string(), z.null()]).optional(),
375
385
  created_at: z.string().datetime({ offset: true }),
376
386
  completed_at: z.union([z.string(), z.null()]),
377
- landings: z.array(LandingResponse).optional()
387
+ landings: z.array(LandingResponse).optional(),
388
+ repeat_index: z.number().int().optional().default(0),
389
+ repeat_total: z.number().int().optional().default(1),
390
+ repeat_session_id: z.union([z.string(), z.null()]).optional(),
391
+ repeat_scan_ids: z.array(z.string().uuid()).optional(),
392
+ retry_attempt: z.number().int().optional().default(0),
393
+ retry_max_attempts: z.number().int().optional().default(0)
378
394
  }).passthrough();
379
395
  z.union([z.string(), z.null()]).optional();
380
396
  var ScanBriefResponse = z.object({
@@ -401,7 +417,12 @@ var ScanBriefResponse = z.object({
401
417
  ad_kind: z.union([z.string(), z.null()]).optional(),
402
418
  network: z.string().optional().default(""),
403
419
  emulator_display_name: z.string().optional().default(""),
404
- emulator_category: z.string().optional().default("")
420
+ emulator_category: z.string().optional().default(""),
421
+ repeat_index: z.number().int().optional().default(0),
422
+ repeat_total: z.number().int().optional().default(1),
423
+ repeat_session_id: z.union([z.string(), z.null()]).optional(),
424
+ retry_attempt: z.number().int().optional().default(0),
425
+ retry_max_attempts: z.number().int().optional().default(0)
405
426
  }).passthrough();
406
427
  z.object({
407
428
  items: z.array(ScanBriefResponse),
@@ -414,10 +435,14 @@ z.object({
414
435
  url: z.union([z.string(), z.null()]).optional(),
415
436
  ad_tag: z.union([z.string(), z.null()]).optional(),
416
437
  vast_tag: z.union([z.string(), z.null()]).optional(),
438
+ referrer: z.union([z.string(), z.null()]).optional(),
417
439
  country_codes: z.array(z.string()).min(1),
418
440
  emulator_id: z.string().min(1).max(100),
419
441
  proxy: ProxyTargetRequest.optional(),
420
- labels: z.record(z.string()).optional()
442
+ labels: z.record(z.string()).optional(),
443
+ repeat_count: z.number().int().gte(1).optional().default(1),
444
+ repeat_mode: RepeatModeType.optional(),
445
+ retry_max_attempts: z.number().int().gte(0).optional().default(0)
421
446
  }).passthrough();
422
447
  z.object({
423
448
  scope_type: z.enum(["last_n", "hours"]),
@@ -461,12 +486,14 @@ var GroupActionResponse = z.object({
461
486
  run_ids: z.array(z.string().uuid()).optional(),
462
487
  failures: z.array(BulkCampaignFailure).optional()
463
488
  }).passthrough();
489
+ var RepeatMode = z.enum(["isolated", "shared"]);
464
490
  z.object({
465
491
  name: z.string().min(1).max(200),
466
492
  campaign_type: z.string().optional().default("url"),
467
493
  url: z.union([z.string(), z.null()]).optional(),
468
494
  ad_tag: z.union([z.string(), z.null()]).optional(),
469
495
  vast_tag: z.union([z.string(), z.null()]).optional(),
496
+ referrer: z.union([z.string(), z.null()]).optional(),
470
497
  country_codes: z.array(z.string()).min(1),
471
498
  group_id: z.union([z.string(), z.null()]).optional(),
472
499
  emulator_categories: z.array(z.string()).optional(),
@@ -476,6 +503,9 @@ z.object({
476
503
  proxy_region: z.string().optional().default(""),
477
504
  proxy_city: z.string().optional().default(""),
478
505
  proxy_isp: z.string().optional().default(""),
506
+ repeat_count: z.number().int().gte(1).optional().default(1),
507
+ repeat_mode: RepeatMode.optional(),
508
+ retry_max_attempts: z.number().int().gte(0).optional().default(0),
479
509
  labels: z.record(z.string()).optional(),
480
510
  policy_set_id: z.union([z.string(), z.null()]).optional(),
481
511
  schedule_type: z.union([z.string(), z.null()]).optional(),
@@ -496,6 +526,7 @@ var CampaignResponse = z.object({
496
526
  url: z.string(),
497
527
  ad_tag: z.union([z.string(), z.null()]).optional(),
498
528
  vast_tag: z.union([z.string(), z.null()]).optional(),
529
+ referrer: z.union([z.string(), z.null()]).optional(),
499
530
  country_codes: z.array(z.string()),
500
531
  group_id: z.string().uuid(),
501
532
  emulator_selection: EmulatorSelectionResponse,
@@ -503,6 +534,9 @@ var CampaignResponse = z.object({
503
534
  proxy_region: z.string().optional().default(""),
504
535
  proxy_city: z.string().optional().default(""),
505
536
  proxy_isp: z.string().optional().default(""),
537
+ repeat_count: z.number().int().optional().default(1),
538
+ repeat_mode: RepeatMode.optional(),
539
+ retry_max_attempts: z.number().int().optional().default(0),
506
540
  schedule_type: z.union([z.string(), z.null()]).optional(),
507
541
  schedule_weekly: z.union([z.record(z.array(z.number().int())), z.null()]).optional(),
508
542
  schedule_interval_seconds: z.union([z.number(), z.null()]).optional(),
@@ -533,6 +567,7 @@ z.object({
533
567
  url: z.union([z.string(), z.null()]),
534
568
  ad_tag: z.union([z.string(), z.null()]),
535
569
  vast_tag: z.union([z.string(), z.null()]),
570
+ referrer: z.union([z.string(), z.null()]),
536
571
  country_codes: z.union([z.array(z.string()), z.null()]),
537
572
  group_id: z.union([z.string(), z.null()]),
538
573
  emulator_categories: z.union([z.array(z.string()), z.null()]),
@@ -542,6 +577,9 @@ z.object({
542
577
  proxy_region: z.union([z.string(), z.null()]),
543
578
  proxy_city: z.union([z.string(), z.null()]),
544
579
  proxy_isp: z.union([z.string(), z.null()]),
580
+ repeat_count: z.union([z.number(), z.null()]),
581
+ repeat_mode: z.union([RepeatMode, z.null()]),
582
+ retry_max_attempts: z.union([z.number(), z.null()]),
545
583
  labels: z.union([z.record(z.string()), z.null()]),
546
584
  policy_set_id: z.union([z.string(), z.null()]),
547
585
  schedule_type: z.union([z.string(), z.null()]),
@@ -649,7 +687,8 @@ z.object({
649
687
  tag_slug: z.string().max(100).optional().default(""),
650
688
  rule_type: z.string().max(50),
651
689
  config: z.object({}).partial().passthrough(),
652
- target: z.string().max(30).optional().default("page")
690
+ target: z.string().max(30).optional().default("page"),
691
+ tag_visibility: z.union([z.record(TagVisibility), z.null()]).optional()
653
692
  }).passthrough();
654
693
  var CustomRuleResponse = z.object({
655
694
  id: z.string().uuid(),
@@ -661,7 +700,8 @@ var CustomRuleResponse = z.object({
661
700
  target: z.string(),
662
701
  is_active: z.boolean(),
663
702
  created_at: z.string().datetime({ offset: true }),
664
- scope: z.string().optional().default("personal")
703
+ scope: z.string().optional().default("personal"),
704
+ tag_visibility: z.record(z.string()).optional()
665
705
  }).passthrough();
666
706
  z.object({
667
707
  items: z.array(CustomRuleResponse),
@@ -675,7 +715,8 @@ z.object({
675
715
  tag_slug: z.union([z.string(), z.null()]),
676
716
  config: z.union([z.object({}).partial().passthrough(), z.null()]),
677
717
  target: z.union([z.string(), z.null()]),
678
- is_active: z.union([z.boolean(), z.null()])
718
+ is_active: z.union([z.boolean(), z.null()]),
719
+ tag_visibility: z.union([z.record(TagVisibility), z.null()])
679
720
  }).partial().passthrough();
680
721
  z.object({
681
722
  scan_id: z.string().uuid(),
@@ -906,7 +947,8 @@ var BalanceTransactionType = z.enum([
906
947
  "admin_adjustment",
907
948
  "refund",
908
949
  "invoice_settlement",
909
- "crypto_top_up"
950
+ "crypto_top_up",
951
+ "card_top_up"
910
952
  ]);
911
953
  z.union([z.array(BalanceTransactionType), z.null()]).optional();
912
954
  var BalanceTransactionResponse = z.object({
@@ -1153,6 +1195,7 @@ z.object({
1153
1195
  description: z.string().max(500).optional().default(""),
1154
1196
  nodes: z.array(TaxonomyNodeRequest).optional().default([])
1155
1197
  });
1198
+ z.enum(["any", "all"]);
1156
1199
  var schemas = {
1157
1200
  OrgResponse,
1158
1201
  LabelDefinitionResponse,
@@ -1166,6 +1209,7 @@ var schemas = {
1166
1209
  EmulatorResponse,
1167
1210
  CampaignGroupResponse,
1168
1211
  GroupActionResponse,
1212
+ RepeatMode,
1169
1213
  CampaignResponse,
1170
1214
  CampaignPickerItem,
1171
1215
  RunResponse,
@@ -1301,6 +1345,7 @@ var CampaignSchema = schemas.CampaignResponse.pick({
1301
1345
  url: true,
1302
1346
  ad_tag: true,
1303
1347
  vast_tag: true,
1348
+ referrer: true,
1304
1349
  country_codes: true,
1305
1350
  group_id: true,
1306
1351
  emulator_selection: true,
@@ -1308,6 +1353,9 @@ var CampaignSchema = schemas.CampaignResponse.pick({
1308
1353
  proxy_region: true,
1309
1354
  proxy_city: true,
1310
1355
  proxy_isp: true,
1356
+ repeat_count: true,
1357
+ repeat_mode: true,
1358
+ retry_max_attempts: true,
1311
1359
  labels: true,
1312
1360
  policy_set_id: true,
1313
1361
  schedule_enabled: true,
@@ -1318,7 +1366,7 @@ var CampaignSchema = schemas.CampaignResponse.pick({
1318
1366
  is_archived: true,
1319
1367
  created_at: true,
1320
1368
  last_run_at: true
1321
- }).strip();
1369
+ }).extend({ repeat_mode: schemas.RepeatMode.default("isolated") }).strip();
1322
1370
  var parseCampaign = (raw) => parseWithSchema(CampaignSchema, raw, "campaign");
1323
1371
  var parseCampaignPage = (raw) => parsePagedWithItemSchema(CampaignSchema, raw, "campaigns");
1324
1372
 
@@ -1731,6 +1779,7 @@ var ScanSchema = schemas.ScanResponse.pick({
1731
1779
  public_report_url: true,
1732
1780
  ad_tag: true,
1733
1781
  vast_tag: true,
1782
+ referrer: true,
1734
1783
  creative_kind: true,
1735
1784
  video: true,
1736
1785
  creative_screenshot_url: true,
@@ -1746,8 +1795,14 @@ var ScanSchema = schemas.ScanResponse.pick({
1746
1795
  ad_kind: true,
1747
1796
  network: true,
1748
1797
  created_at: true,
1749
- completed_at: true
1750
- }).strip();
1798
+ completed_at: true,
1799
+ repeat_index: true,
1800
+ repeat_total: true,
1801
+ repeat_session_id: true,
1802
+ repeat_scan_ids: true,
1803
+ retry_attempt: true,
1804
+ retry_max_attempts: true
1805
+ }).extend({ creative_kind: z.string().default("banner") }).strip();
1751
1806
  var ScanArraySchema = z.array(ScanSchema);
1752
1807
  var parseScan = (raw) => parseWithSchema(ScanSchema, raw, "scan");
1753
1808
  var parseScanArray = (raw) => parseWithSchema(ScanArraySchema, raw, "scans");
@@ -1773,7 +1828,12 @@ var ScanBriefSchema = schemas.ScanBriefResponse.pick({
1773
1828
  slot_index: true,
1774
1829
  ad_kind: true,
1775
1830
  network: true,
1776
- created_at: true
1831
+ created_at: true,
1832
+ repeat_index: true,
1833
+ repeat_total: true,
1834
+ repeat_session_id: true,
1835
+ retry_attempt: true,
1836
+ retry_max_attempts: true
1777
1837
  }).strip();
1778
1838
  var parseScanPage = (raw) => parsePagedWithItemSchema(ScanBriefSchema, raw, "scans");
1779
1839
  var TagDefinitionSchema = schemas.TagDefinitionWithStatsResponse.pick({
@@ -1865,10 +1925,7 @@ function createHttpApiGateway(config) {
1865
1925
  if (dispatcher !== void 0) {
1866
1926
  baseInit["dispatcher"] = dispatcher;
1867
1927
  }
1868
- return fetch(
1869
- input.url,
1870
- baseInit
1871
- );
1928
+ return fetch(input.url, baseInit);
1872
1929
  };
1873
1930
  const client = createClient({
1874
1931
  baseUrl,
@@ -1933,10 +1990,7 @@ function createHttpApiGateway(config) {
1933
1990
  }
1934
1991
  };
1935
1992
  if (dispatcher !== void 0) init["dispatcher"] = dispatcher;
1936
- response = await fetch(
1937
- url,
1938
- init
1939
- );
1993
+ response = await fetch(url, init);
1940
1994
  } catch (cause) {
1941
1995
  logger.warn({ api_path: path, elapsed_ms: Date.now() - startedAtMs }, "api.network_error");
1942
1996
  const inner = cause instanceof Error && cause.cause instanceof Error ? cause.cause : cause instanceof Error ? cause : null;
@@ -3245,13 +3299,14 @@ var TransactionTypeEnum = z.enum([
3245
3299
  "admin_adjustment",
3246
3300
  "refund",
3247
3301
  "invoice_settlement",
3248
- "crypto_top_up"
3302
+ "crypto_top_up",
3303
+ "card_top_up"
3249
3304
  ]);
3250
3305
  var ListBalanceHistoryInputShape = {
3251
3306
  date_from: z.string().date().optional().describe("ISO date, inclusive."),
3252
3307
  date_to: z.string().date().optional().describe("ISO date, inclusive."),
3253
- type: z.array(TransactionTypeEnum).max(9).optional().describe(
3254
- "Filter by transaction kind. Pass several values to OR them (e.g. ['top_up_manual','crypto_top_up'] for credits-only)."
3308
+ type: z.array(TransactionTypeEnum).max(TransactionTypeEnum.options.length).optional().describe(
3309
+ "Filter by transaction kind. Pass several values to OR them (e.g. ['top_up_manual','crypto_top_up','card_top_up'] for credits-only)."
3255
3310
  ),
3256
3311
  page: z.number().int().min(1).max(500).default(1).describe("1-indexed page."),
3257
3312
  limit: z.number().int().min(1).max(200).default(50).describe("Page size.")
@@ -3558,6 +3613,28 @@ var cancelCampaignTool = {
3558
3613
  return ok(result.value);
3559
3614
  }
3560
3615
  };
3616
+ var repeatRetryFields = {
3617
+ repeat_count: z.number().int().min(1).max(20).optional().describe(
3618
+ "How many times to repeat every scan this call already produces (1-20). It is a multiplier on top of the tool's own fan-out \u2014 see that tool's description for the exact formula \u2014 and each resulting repeat is a full scan with its own report and its own billing. 1-20 is the API's default ceiling; an operator can lower it during an incident, and then a value accepted here still comes back as a 422 from the API. Default: 1."
3619
+ ),
3620
+ repeat_mode: z.enum(["isolated", "shared"]).optional().describe(
3621
+ "How the repeats of one combination relate to each other. 'isolated' gives every repeat a fresh browser and a new IP, so the repeats are statistically independent. 'shared' runs all repeats of one combination in a single browser behind one IP, carrying cookies and localStorage from one repeat to the next \u2014 use it to reproduce a cloaker or a frequency cap that only misbehaves on the second or third visit. 'shared' is rejected with 422 on an ad-discovery target: when creating, that is `ad_discovery: true` on a scan or `campaign_type: \"ad_discovery\"` on a campaign; when updating a campaign, it is a campaign whose existing type is already ad_discovery, since campaign_type itself cannot be changed. Default: isolated."
3622
+ ),
3623
+ retry_max_attempts: z.number().int().min(0).max(5).optional().describe(
3624
+ "Extra crawl attempts when a scan fails for a technical reason \u2014 dead proxy, navigation timeout, browser crash (0-5). Permanent failures are never retried. The same scan is reused and only a completed scan is billed, so a retry never double-charges. 0-5 is the API's default ceiling; an operator can lower it, and then a value accepted here still comes back as a 422 from the API. Default: 0."
3625
+ )
3626
+ };
3627
+ function pickRepeatRetryBody(input) {
3628
+ const body = {};
3629
+ if (input.repeat_count !== void 0) body.repeat_count = input.repeat_count;
3630
+ if (input.repeat_mode !== void 0) body.repeat_mode = input.repeat_mode;
3631
+ if (input.retry_max_attempts !== void 0) {
3632
+ body.retry_max_attempts = input.retry_max_attempts;
3633
+ }
3634
+ return body;
3635
+ }
3636
+
3637
+ // src/application/tools/campaigns/_campaign-config-fields.ts
3561
3638
  var campaignConfigFields = {
3562
3639
  emulator_categories: z.array(z.string()).optional().describe(
3563
3640
  "Device categories to rotate through, e.g. ['android_phone']. Valid: android_phone, android_tablet, iphone, ipad, windows_desktop, macos_desktop. On create, omitting this defaults to ['android_phone']; pass [] together with emulator_specific_ids to target ONLY specific devices."
@@ -3572,6 +3649,7 @@ var campaignConfigFields = {
3572
3649
  proxy_region: z.string().optional().describe("Proxy region/state targeting (free-text; only honoured for a single country)."),
3573
3650
  proxy_city: z.string().optional().describe("Proxy city targeting."),
3574
3651
  proxy_isp: z.string().optional().describe("Proxy ISP targeting."),
3652
+ ...repeatRetryFields,
3575
3653
  schedule_type: z.enum(["weekly", "interval"]).optional().describe(
3576
3654
  "Scheduling mode: 'weekly' (run on a weekday/hour grid via schedule_weekly) or 'interval' (run every schedule_interval_seconds). Omit for a manual (run-on-demand) campaign."
3577
3655
  ),
@@ -3584,7 +3662,7 @@ var campaignConfigFields = {
3584
3662
  schedule_timezone: z.string().optional().describe("IANA timezone for the weekly grid, e.g. 'Europe/Berlin'. Default: UTC.")
3585
3663
  };
3586
3664
  function pickCampaignConfigBody(input) {
3587
- const body = {};
3665
+ const body = { ...pickRepeatRetryBody(input) };
3588
3666
  if (input.emulator_categories !== void 0) body.emulator_categories = input.emulator_categories;
3589
3667
  if (input.emulator_specific_ids !== void 0) {
3590
3668
  body.emulator_specific_ids = input.emulator_specific_ids;
@@ -3602,6 +3680,10 @@ function pickCampaignConfigBody(input) {
3602
3680
  if (input.schedule_timezone !== void 0) body.schedule_timezone = input.schedule_timezone;
3603
3681
  return body;
3604
3682
  }
3683
+ var campaignReferrerUrl = z.string().url().regex(/^https?:\/\//i, "Must be an http(s) URL.").max(2048);
3684
+ var CAMPAIGN_REFERRER_DESCRIPTION = "Optional http(s) page URL every scan of this campaign is checked from. For `ad_tag` and `vast` campaigns this is the publisher page the tag is embedded in \u2014 NOT the creative and NOT its landing page; the browser commits the page document on this URL without fetching the publisher, so the creative renders as if embedded there, and its cross-origin subrequests receive the origin only (https://publisher.example/, no path). For `url` and `ad_discovery` campaigns this is where the visitor came from, sent in full as the Referer of the page request.";
3685
+ var campaignReferrerField = campaignReferrerUrl.optional().describe(CAMPAIGN_REFERRER_DESCRIPTION);
3686
+ var campaignReferrerUpdateField = campaignReferrerUrl.nullable().optional().describe(`${CAMPAIGN_REFERRER_DESCRIPTION} Pass null to clear it.`);
3605
3687
 
3606
3688
  // src/application/tools/campaigns/create-campaign.tool.ts
3607
3689
  var CreateCampaignInputShape = {
@@ -3616,6 +3698,7 @@ var CreateCampaignInputShape = {
3616
3698
  vast_tag: z.string().optional().describe(
3617
3699
  "VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (required if campaign_type=vast)."
3618
3700
  ),
3701
+ referrer: campaignReferrerField,
3619
3702
  country_codes: z.array(z.string().length(2)).min(1).describe("ISO 3166-1 alpha-2 codes \u2014 one scan per country per run."),
3620
3703
  group_id: z.string().uuid().optional().describe("Parent group UUID; defaults to the org's default group."),
3621
3704
  ...campaignConfigFields,
@@ -3625,7 +3708,7 @@ var CreateCampaignInputShape = {
3625
3708
  };
3626
3709
  var createCampaignTool = {
3627
3710
  name: "create_campaign",
3628
- description: "Create a recurring scan campaign (template). The schedule produces N scans per run where N = number of countries times number of device profiles. Scans cost credits when they run, not when the campaign is created.",
3711
+ description: "Create a recurring scan campaign (template). The schedule produces N scans per run where N = number of countries times number of device profiles times `repeat_count`. Scans cost credits when they run, not when the campaign is created, so a high `repeat_count` multiplies the bill on every run. The returned campaign echoes `repeat_count`, `repeat_mode`, and `retry_max_attempts`, so read it back to confirm what the campaign will do.",
3629
3712
  annotations: {
3630
3713
  title: "Create Campaign",
3631
3714
  readOnlyHint: false,
@@ -3642,6 +3725,7 @@ var createCampaignTool = {
3642
3725
  ...input.url !== void 0 ? { url: input.url } : {},
3643
3726
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
3644
3727
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
3728
+ ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
3645
3729
  ...input.group_id !== void 0 ? { group_id: input.group_id } : {},
3646
3730
  ...input.labels !== void 0 ? { labels: input.labels } : {},
3647
3731
  ...input.policy_set_id !== void 0 ? { policy_set_id: input.policy_set_id } : {},
@@ -3658,7 +3742,7 @@ var GetCampaignInputShape = {
3658
3742
  };
3659
3743
  var getCampaignTool = {
3660
3744
  name: "get_campaign",
3661
- description: "Get one campaign by UUID: name, type (url|ad_tag|vast|ad_discovery), target URL / ad-tag / VAST tag (vast_tag), countries, schedule status, archive status, parent group. `ad_discovery` campaigns store the publisher page in the URL field.",
3745
+ description: "Get one campaign by UUID: name, type (url|ad_tag|vast|ad_discovery), target URL / ad-tag / VAST tag (vast_tag), countries, emulator selection, proxy targeting, schedule status, archive status, parent group. Also echoes the repeat / retry settings \u2014 `repeat_count`, `repeat_mode`, `retry_max_attempts` \u2014 so this is how you confirm what `create_campaign` or `update_campaign` actually saved, and how many scans a run will produce (countries x device profiles x `repeat_count`). `ad_discovery` campaigns store the publisher page in the URL field.",
3662
3746
  annotations: {
3663
3747
  title: "Get Campaign",
3664
3748
  readOnlyHint: true,
@@ -3709,7 +3793,7 @@ var ListCampaignsInputShape = {
3709
3793
  };
3710
3794
  var listCampaignsTool = {
3711
3795
  name: "list_campaigns",
3712
- description: "List campaigns for the caller's organization, optionally filtered by group, archived flag, or name substring. Paginated.",
3796
+ description: "List campaigns for the caller's organization, optionally filtered by group, archived flag, or name substring. Paginated. Each row carries the same fields as `get_campaign`, including the repeat / retry settings (`repeat_count`, `repeat_mode`, `retry_max_attempts`) \u2014 use them to spot the campaigns that multiply their per-run scan count.",
3713
3797
  annotations: {
3714
3798
  title: "List Campaigns",
3715
3799
  readOnlyHint: true,
@@ -3812,6 +3896,7 @@ var UpdateCampaignInputShape = {
3812
3896
  vast_tag: z.string().optional().describe(
3813
3897
  "New VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (vast-type campaigns)."
3814
3898
  ),
3899
+ referrer: campaignReferrerUpdateField,
3815
3900
  country_codes: z.array(z.string().length(2)).optional().describe("Replace the country list."),
3816
3901
  group_id: z.string().uuid().optional().describe("Move the campaign to another group."),
3817
3902
  ...campaignConfigFields,
@@ -3821,7 +3906,7 @@ var UpdateCampaignInputShape = {
3821
3906
  };
3822
3907
  var updateCampaignTool = {
3823
3908
  name: "update_campaign",
3824
- description: "Update one or more fields of a campaign. Fields not supplied are left unchanged. `policy_set_id` accepts null to clear the binding.",
3909
+ description: "Update one or more fields of a campaign. Fields not supplied are left unchanged. `policy_set_id` accepts null to clear the binding, and `referrer` accepts null to clear the publisher page scans are checked from.",
3825
3910
  annotations: {
3826
3911
  title: "Update Campaign",
3827
3912
  readOnlyHint: false,
@@ -3836,6 +3921,7 @@ var updateCampaignTool = {
3836
3921
  ...input.url !== void 0 ? { url: input.url } : {},
3837
3922
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
3838
3923
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
3924
+ ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
3839
3925
  ...input.country_codes !== void 0 ? { country_codes: input.country_codes } : {},
3840
3926
  ...input.group_id !== void 0 ? { group_id: input.group_id } : {},
3841
3927
  ...input.labels !== void 0 ? { labels: input.labels } : {},
@@ -3848,6 +3934,21 @@ var updateCampaignTool = {
3848
3934
  return ok(result.value);
3849
3935
  }
3850
3936
  };
3937
+ var COMBO_MATCH_SCOPE_DOC = 'For `rule_type=\'combo\'` the optional rule-level key `match_scope` decides where conditions are counted. `"scan"` (the default, and the behaviour when the key is absent) checks them against the union of all tags on the scan. With `"url"`, every condition must be satisfied by tags on the same link, and the tag is assigned to that link \u2014 so a rule built only from link-less tags (AI verdicts, crawler behaviour) has no link to attach to and will never match; keep those on the whole scan. Mixing the two kinds does not help either: AI verdicts and per-link detections reach the engine in different scan parts and are not evaluated together today, so such a combo fires under neither scope. Give a `"url"` rule at least one positive condition (`all_of` / `any_of` / `tag_category` + `count_gte`) \u2014 a config carrying only `none_of` matches every link that merely lacks those tags, and the API does not reject that on this path. Thresholds stay "N or more" (`count_gte` / `any_of_min`) in both scopes; no other `match_scope` value is accepted.';
3938
+ var COMBO_MATCH_SCOPE_READ_DOC = 'For `rule_type=\'combo\'` the returned `config` may carry the rule-level key `match_scope` ("scan" = count conditions across the whole scan, "url" = every condition must be satisfied by tags on the same link). Resend it verbatim when updating: `update_custom_rule` replaces `config` wholesale, so dropping the key reverts the rule to whole-scan matching.';
3939
+ var ruleConfigField = z.record(z.unknown()).superRefine((config, ctx) => {
3940
+ const scope = config["match_scope"];
3941
+ if (scope === void 0 || scope === "scan" || scope === "url") {
3942
+ return;
3943
+ }
3944
+ ctx.addIssue({
3945
+ code: z.ZodIssueCode.custom,
3946
+ path: ["match_scope"],
3947
+ message: 'config.match_scope must be "scan" or "url". Omit the key for scan-wide matching (the default).'
3948
+ });
3949
+ });
3950
+
3951
+ // src/application/tools/custom-rules/create-custom-rule.tool.ts
3851
3952
  var CreateCustomRuleInputShape = {
3852
3953
  name: z.string().min(1).max(200).describe(
3853
3954
  "Display name. For non-LLM rules this also becomes the `display_name` of the auto-registered tag definition (see `tag_slug`)."
@@ -3858,8 +3959,8 @@ var CreateCustomRuleInputShape = {
3858
3959
  rule_type: z.string().max(50).describe(
3859
3960
  "Rule engine. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `blacklist_domain`, `combo`, `llm`. The API validates."
3860
3961
  ),
3861
- config: z.record(z.unknown()).describe(
3862
- "Rule-type-specific configuration object. Shape depends on `rule_type`. For `rule_type='llm'` the shape is `{ prompt: string, tags: { <tag_slug>: <description>, ... } }`; each key in `config.tags` is auto-registered as a custom tag definition AND must not collide with a system slug (same 422 contract as `tag_slug`)."
3962
+ config: ruleConfigField.describe(
3963
+ "Rule-type-specific configuration object. Shape depends on `rule_type`. For `rule_type='llm'` the shape is `{ prompt: string, tags: { <tag_slug>: <description>, ... } }`; each key in `config.tags` is auto-registered as a custom tag definition AND must not collide with a system slug (same 422 contract as `tag_slug`). " + COMBO_MATCH_SCOPE_DOC
3863
3964
  ),
3864
3965
  target: z.string().max(30).optional().describe(
3865
3966
  "Where to apply the rule (e.g. 'page' for landing HTML). Default: page. See API docs for the full set of valid values."
@@ -3867,7 +3968,7 @@ var CreateCustomRuleInputShape = {
3867
3968
  };
3868
3969
  var createCustomRuleTool = {
3869
3970
  name: "create_custom_rule",
3870
- description: "Define a custom tag-detection rule. The API auto-registers a tag definition for each slug the rule emits (`tag_slug` for non-LLM rules; `config.tags` keys for `rule_type='llm'`); slugs that collide with a built-in system tag are rejected with HTTP 422 / code `checking.system_slug_reserved`. Matches tag every future scan; existing scans are untouched until you call `recheck_scans`.",
3971
+ description: "Define a custom tag-detection rule. The API auto-registers a tag definition for each slug the rule emits (`tag_slug` for non-LLM rules; `config.tags` keys for `rule_type='llm'`); slugs that collide with a built-in system tag are rejected with HTTP 422 / code `checking.system_slug_reserved`. For `rule_type='combo'` set `config.match_scope='url'` when the thresholds must be met inside one link instead of anywhere on the scan (default `'scan'`). Matches tag every future scan; existing scans are untouched until you call `recheck_scans`.",
3871
3972
  annotations: {
3872
3973
  title: "Create Custom Rule",
3873
3974
  readOnlyHint: false,
@@ -3912,7 +4013,7 @@ var deleteCustomRuleTool = {
3912
4013
  var GetCustomRuleInputShape = { rule_id: z.string().uuid().describe("Rule UUID.") };
3913
4014
  var getCustomRuleTool = {
3914
4015
  name: "get_custom_rule",
3915
- description: "Get one custom rule by UUID with name, tag-slug, type, config object, target, active flag.",
4016
+ description: "Get one custom rule by UUID with name, tag-slug, type, config object, target, active flag. " + COMBO_MATCH_SCOPE_READ_DOC,
3916
4017
  annotations: {
3917
4018
  title: "Get Custom Rule",
3918
4019
  readOnlyHint: true,
@@ -3933,7 +4034,7 @@ var ListCustomRulesInputShape = {
3933
4034
  };
3934
4035
  var listCustomRulesTool = {
3935
4036
  name: "list_custom_rules",
3936
- description: "Paginated list of the organization's custom tag-detection rules (regex / heuristics) with their config, target, and active flag. Returns `{items, total, page, limit}`. To iterate: there is a next page iff `page * limit < total` (or equivalently `items.length === limit` until the last page). `page` is 1-indexed.",
4037
+ description: "Paginated list of the organization's custom tag-detection rules (regex / heuristics) with their config, target, and active flag. Returns `{items, total, page, limit}`. To iterate: there is a next page iff `page * limit < total` (or equivalently `items.length === limit` until the last page). `page` is 1-indexed. " + COMBO_MATCH_SCOPE_READ_DOC,
3937
4038
  annotations: {
3938
4039
  title: "List Custom Rules",
3939
4040
  readOnlyHint: true,
@@ -3952,8 +4053,8 @@ var TestCustomRuleInputShape = {
3952
4053
  rule_type: z.string().max(50).describe(
3953
4054
  "Rule engine type. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `blacklist_domain`, `combo`, `llm`."
3954
4055
  ),
3955
- config: z.record(z.unknown()).describe(
3956
- "Rule-type-specific config to test. Same shape as `create_custom_rule`'s `config`. NOTE: `test_custom_rule` evaluates the rule against a scan WITHOUT persisting it, so slug-collision validation does NOT run here \u2014 verify slugs against `list_tags` (`scope=system`) before promoting to `create_custom_rule`."
4056
+ config: ruleConfigField.describe(
4057
+ "Rule-type-specific config to test. Same shape as `create_custom_rule`'s `config`. NOTE: `test_custom_rule` evaluates the rule against a scan WITHOUT persisting it, so slug-collision validation does NOT run here \u2014 verify slugs against `list_tags` (`scope=system`) before promoting to `create_custom_rule`. " + COMBO_MATCH_SCOPE_DOC
3957
4058
  ),
3958
4059
  target: z.string().max(30).describe(
3959
4060
  "Where to apply the rule (e.g. 'page' for landing HTML). See API docs for the full set of valid values."
@@ -3962,7 +4063,7 @@ var TestCustomRuleInputShape = {
3962
4063
  };
3963
4064
  var testCustomRuleTool = {
3964
4065
  name: "test_custom_rule",
3965
- description: "Preview-test a rule definition against an existing scan WITHOUT persisting the rule. Returns `matched: bool`, evaluation time, and per-tag-slug details. Use to validate config before `create_custom_rule`. NOTE: slug-collision validation does NOT run in preview mode \u2014 verify any `tag_slug` (or LLM `config.tags` keys) against `list_tags` (`scope=system`) before promoting to `create_custom_rule`, which would otherwise reject the same payload with HTTP 422 / code `checking.system_slug_reserved`.",
4066
+ description: "Preview-test a rule definition against an existing scan WITHOUT persisting the rule. Returns `matched: bool`, evaluation time, and per-tag-slug details. Use to validate config before `create_custom_rule`. Combo rules are evaluated against the scan's existing tags, so `config.match_scope` is honoured \u2014 but per-URL hits are collapsed to one row per tag slug, so the result says whether the rule fires, not which URLs it would tag. NOTE: slug-collision validation does NOT run in preview mode \u2014 verify any `tag_slug` (or LLM `config.tags` keys) against `list_tags` (`scope=system`) before promoting to `create_custom_rule`, which would otherwise reject the same payload with HTTP 422 / code `checking.system_slug_reserved`.",
3966
4067
  annotations: {
3967
4068
  title: "Test Custom Rule",
3968
4069
  readOnlyHint: true,
@@ -3990,8 +4091,8 @@ var UpdateCustomRuleInputShape = {
3990
4091
  tag_slug: z.string().max(100).optional().describe(
3991
4092
  "New tag slug to assign on match. **MUST NOT collide with a built-in system tag slug** (see `list_tags` where `scope=system`); colliding requests return 422 with code `checking.system_slug_reserved`. Re-registering an existing custom slug refreshes its tag definition's `display_name` / `description`."
3992
4093
  ),
3993
- config: z.record(z.unknown()).optional().describe(
3994
- "New rule-type-specific config object. For `rule_type='llm'` the keys of `config.tags` are auto-registered as tag definitions; any key that collides with a system slug returns the same 422 contract."
4094
+ config: ruleConfigField.optional().describe(
4095
+ "New rule-type-specific config object. Replaces the stored config wholesale \u2014 resend every key you want to keep, including a combo rule's `match_scope`. For `rule_type='llm'` the keys of `config.tags` are auto-registered as tag definitions; any key that collides with a system slug returns the same 422 contract. " + COMBO_MATCH_SCOPE_DOC
3995
4096
  ),
3996
4097
  target: z.string().max(30).optional().describe(
3997
4098
  "Where to apply the rule (e.g. 'page' for landing HTML). See API docs for the full set of valid values."
@@ -4000,7 +4101,7 @@ var UpdateCustomRuleInputShape = {
4000
4101
  };
4001
4102
  var updateCustomRuleTool = {
4002
4103
  name: "update_custom_rule",
4003
- description: "Update a custom tag-detection rule. Only supplied fields are sent. Changing `tag_slug` (or `config.tags` keys for `rule_type='llm'`) onto a built-in system slug returns HTTP 422 / code `checking.system_slug_reserved`. To toggle activation, pass `is_active`. Existing tagged scans are NOT re-evaluated \u2014 call `recheck_scans` for that. (Rule engine `rule_type` cannot be changed after creation; create a new rule instead.)",
4104
+ description: "Update a custom tag-detection rule. Only supplied fields are sent, but a supplied `config` replaces the stored one wholesale (omitting a combo rule's `match_scope` reverts it to the scan-wide default). Changing `tag_slug` (or `config.tags` keys for `rule_type='llm'`) onto a built-in system slug returns HTTP 422 / code `checking.system_slug_reserved`. To toggle activation, pass `is_active`. Existing tagged scans are NOT re-evaluated \u2014 call `recheck_scans` for that. (Rule engine `rule_type` cannot be changed after creation; create a new rule instead.)",
4004
4105
  annotations: {
4005
4106
  title: "Update Custom Rule",
4006
4107
  readOnlyHint: false,
@@ -4663,6 +4764,9 @@ var scanProxyField = z.object({
4663
4764
  city: z.string().optional().describe("Proxy city targeting."),
4664
4765
  isp: z.string().optional().describe("Proxy ISP targeting.")
4665
4766
  }).optional().describe("Optional proxy geo targeting for the crawl. Omit to use the org default.");
4767
+ var scanReferrerField = z.string().url().regex(/^https?:\/\//i, "Must be an http(s) URL.").max(2048).optional().describe(
4768
+ "Optional http(s) page URL the check is performed from. With `ad_tag` or `vast_tag` this is the publisher page the tag is embedded in \u2014 NOT the creative and NOT its landing page; the browser commits the page document on this URL without fetching the publisher, so the creative renders as if embedded there, and its cross-origin subrequests receive the origin only (https://publisher.example/, no path). With a `url` target this is where the visitor came from, sent in full as the Referer of the page request."
4769
+ );
4666
4770
 
4667
4771
  // src/application/tools/scans/create-bulk-scans.tool.ts
4668
4772
  var CreateBulkScansInputShape = {
@@ -4673,14 +4777,16 @@ var CreateBulkScansInputShape = {
4673
4777
  vast_tag: z.string().optional().describe(
4674
4778
  "VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML. Provide exactly one of `url`, `ad_tag`, or `vast_tag`."
4675
4779
  ),
4780
+ referrer: scanReferrerField,
4676
4781
  country_codes: z.array(z.string().length(2)).min(1).max(50).describe("List of ISO 3166-1 alpha-2 country codes; one scan per country is created."),
4677
4782
  emulator_id: z.string().min(1).max(100).describe("Device/OS profile slug; same for every country in the batch."),
4678
4783
  proxy: scanProxyField,
4679
- labels: z.record(z.string()).optional().describe("Arbitrary metadata copied onto every created scan.")
4784
+ labels: z.record(z.string()).optional().describe("Arbitrary metadata copied onto every created scan."),
4785
+ ...repeatRetryFields
4680
4786
  };
4681
4787
  var createBulkScansTool = {
4682
4788
  name: "create_bulk_scans",
4683
- description: "Queue one new scan per country in a single call (e.g. test the same URL, ad-tag, or VAST video tag from US + DE + JP). COSTS N CREDITS where N = number of countries. Returns the list of created scans.",
4789
+ description: "Queue one new scan per country in a single call (e.g. test the same URL, ad-tag, or VAST video tag from US + DE + JP). COSTS N CREDITS where N = number of countries times `repeat_count`. Returns one entry per country; each entry's `repeat_scan_ids` lists that country's extra repeats.",
4684
4790
  annotations: {
4685
4791
  title: "Create Bulk Scans",
4686
4792
  readOnlyHint: false,
@@ -4696,8 +4802,10 @@ var createBulkScansTool = {
4696
4802
  ...input.url !== void 0 ? { url: input.url } : {},
4697
4803
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
4698
4804
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
4805
+ ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
4699
4806
  ...input.proxy !== void 0 ? { proxy: input.proxy } : {},
4700
- ...input.labels !== void 0 ? { labels: input.labels } : {}
4807
+ ...input.labels !== void 0 ? { labels: input.labels } : {},
4808
+ ...pickRepeatRetryBody(input)
4701
4809
  };
4702
4810
  const result = await ctx.api.createBulkScans(body);
4703
4811
  if (result.isErr()) return err(mapApiError(result.error));
@@ -4714,6 +4822,7 @@ var CreateScanInputShape = {
4714
4822
  vast_tag: z.string().optional().describe(
4715
4823
  "VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (a document containing a <VAST> element). Fetched and played in a real browser. Provide exactly one of `url`, `ad_tag`, or `vast_tag`."
4716
4824
  ),
4825
+ referrer: scanReferrerField,
4717
4826
  country_code: z.string().length(2).describe("ISO 3166-1 alpha-2 country code, e.g. US, DE, JP. Determines proxy geo."),
4718
4827
  emulator_id: z.string().min(1).max(100).describe("Device/OS profile slug; use `list_emulators` to discover valid values."),
4719
4828
  proxy: scanProxyField,
@@ -4722,11 +4831,12 @@ var CreateScanInputShape = {
4722
4831
  run_id: z.string().uuid().optional().describe("Optional run UUID inside the campaign."),
4723
4832
  ad_discovery: z.boolean().optional().describe(
4724
4833
  "Publisher ad discovery: detect ad blocks on the page and spawn one child scan per detected ad (banner/pop). Only valid with `url`. Each child is a separate billed scan; list them with `list_scan_children`."
4725
- )
4834
+ ),
4835
+ ...repeatRetryFields
4726
4836
  };
4727
4837
  var createScanTool = {
4728
4838
  name: "create_scan",
4729
- description: "Queue a single new scan for a URL, ad-tag, or VAST video tag against one country. COSTS CREDITS and bills the caller's organization. Returns the newly-created scan record.",
4839
+ description: "Queue a single new scan for a URL, ad-tag, or VAST video tag against one country. Takes one target and one country \u2014 there is no multi-URL form; call it once per URL. COSTS CREDITS and bills the caller's organization: this call creates exactly `repeat_count` scans (default 1) and bills every one of them. Returns the newly-created scan record; when `repeat_count` > 1 the response's `repeat_scan_ids` lists the sibling scans this call also created.",
4730
4840
  annotations: {
4731
4841
  title: "Create Scan",
4732
4842
  readOnlyHint: false,
@@ -4742,11 +4852,13 @@ var createScanTool = {
4742
4852
  ...input.url !== void 0 ? { url: input.url } : {},
4743
4853
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
4744
4854
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
4855
+ ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
4745
4856
  ...input.proxy !== void 0 ? { proxy: input.proxy } : {},
4746
4857
  ...input.labels !== void 0 ? { labels: input.labels } : {},
4747
4858
  ...input.campaign_id !== void 0 ? { campaign_id: input.campaign_id } : {},
4748
4859
  ...input.run_id !== void 0 ? { run_id: input.run_id } : {},
4749
- ...input.ad_discovery !== void 0 ? { ad_discovery: input.ad_discovery } : {}
4860
+ ...input.ad_discovery !== void 0 ? { ad_discovery: input.ad_discovery } : {},
4861
+ ...pickRepeatRetryBody(input)
4750
4862
  };
4751
4863
  const result = await ctx.api.createScan(body);
4752
4864
  if (result.isErr()) return err(mapApiError(result.error));
@@ -4758,7 +4870,7 @@ var GetScanInputShape = {
4758
4870
  };
4759
4871
  var getScanTool = {
4760
4872
  name: "get_scan",
4761
- description: "Get full detail for one scan by UUID: status, offer URL, absolute screenshot URL, report_url + public_report_url deep-links, timing, labels, and the parent campaign if any. VAST video scans also carry `vast_tag`, `creative_kind` (banner|video), and a `video` block (duration, media-file URL, VAST version, ad system, VPAID flag, wrapper depth). Link users with the returned `report_url` / `public_report_url` \u2014 never construct URLs yourself.",
4873
+ description: "Get full detail for one scan by UUID: status, offer URL, absolute screenshot URL, report_url + public_report_url deep-links, timing, labels, and the parent campaign if any. VAST video scans also carry `vast_tag`, `creative_kind` (`banner` or `video` today; treat it as an open string), and a `video` block (duration, media-file URL, the creative's `click_through` destination, VAST version, ad system, VPAID flag, wrapper depth). Repeats and retries: `repeat_index` / `repeat_total` place this scan inside its repeat group (0-based, so 2 of 5 reads as repeat_index 1), a non-null `repeat_session_id` means the group ran in `shared` mode (same browser, same IP, cookies carried over) and is the key to correlate its members \u2014 a shared group of one has nothing to carry over, so it stays null \u2014 `repeat_scan_ids` is filled only on the create response, and `retry_attempt` / `retry_max_attempts` show how many technical re-crawls this scan already consumed. Link users with the returned `report_url` / `public_report_url` \u2014 never construct URLs yourself.",
4762
4874
  annotations: {
4763
4875
  title: "Get Scan",
4764
4876
  readOnlyHint: true,
@@ -4797,7 +4909,9 @@ var getScanCreativeScreenshotTool = {
4797
4909
  var GetScanLandingScreenshotInputShape = {
4798
4910
  scan_id: z.string().uuid().describe("Scan UUID (ad-tag scan)."),
4799
4911
  landing_ord: z.number().int().min(0).max(50).describe("0-indexed landing slot (see `get_scan(...).landings[*].ord`)."),
4800
- width: z.number().int().min(50).max(2e3).optional().describe("Optional resize width in pixels.")
4912
+ width: z.number().int().min(50).max(2e3).optional().describe(
4913
+ "Optional resize width in pixels. The API top-crops a resized landing screenshot whose height exceeds 2.5x its width, so a long landing page comes back as its top region only \u2014 never conclude that something is absent from the page from a resized capture. Fetch without `width` when you need the whole page."
4914
+ )
4801
4915
  };
4802
4916
  var getScanLandingScreenshotTool = {
4803
4917
  name: "get_scan_landing_screenshot",
@@ -4823,7 +4937,7 @@ var getScanLandingScreenshotTool = {
4823
4937
  var GetScanScreenshotInputShape = {
4824
4938
  scan_id: z.string().uuid().describe("Scan UUID."),
4825
4939
  width: z.number().int().min(50).max(2e3).optional().describe(
4826
- "Optional resize width in pixels. The API resizes server-side to keep the image small for the agent's context window."
4940
+ "Optional resize width in pixels. The API resizes server-side to keep the image small for the agent's context window. It also top-crops a resized page screenshot whose height exceeds 2.5x its width, so a long page comes back as its top region only \u2014 never conclude that something is absent from the page from a resized capture. Fetch without `width` when you need the whole page."
4827
4941
  )
4828
4942
  };
4829
4943
  var getScanScreenshotTool = {
@@ -4850,7 +4964,7 @@ var ListScanChildrenInputShape = {
4850
4964
  };
4851
4965
  var listScanChildrenTool = {
4852
4966
  name: "list_scan_children",
4853
- description: "List the discovered-ad child scans of a publisher ad-discovery scan \u2014 one per detected ad block on the page. Each child brief carries ad_kind (banner|pop), the attributed ad network, and slot_index. Returns a paginated envelope with screenshot + report deep-links; link users with those, never hand-build URLs.",
4967
+ description: "List the discovered-ad child scans of a publisher ad-discovery scan \u2014 one per detected ad block on the page. Each child brief carries ad_kind (banner|pop), the attributed ad network, and slot_index. The repeat / retry fields are present but never meaningful here: a child is always its own group of one (`repeat_index` 0, `repeat_total` 1, `repeat_session_id` null) and is never re-crawled, because repeats and retries apply to the publisher scan \u2014 repeating an ad-discovery scan gives you several parents, each with its own children. Returns a paginated envelope with screenshot + report deep-links; link users with those, never hand-build URLs.",
4854
4968
  annotations: {
4855
4969
  title: "List Scan Children",
4856
4970
  readOnlyHint: true,
@@ -4901,7 +5015,7 @@ var ListScansInputShape = {
4901
5015
  };
4902
5016
  var listScansTool = {
4903
5017
  name: "list_scans",
4904
- description: "List scans for the caller's organization with optional filters (status, country, URL substring, date range, run/campaign/group, tag, AI/IAB/brand category, dynamic labels). Returns a paginated envelope; each brief carries `is_ad_tag` and `is_vast` flags, an absolute screenshot URL plus report_url (auth dashboard) and public_report_url (shareable) deep-links \u2014 link users with those, never hand-build URLs.",
5018
+ description: "List scans for the caller's organization with optional filters (status, country, URL substring, date range, run/campaign/group, tag, AI/IAB/brand category, dynamic labels). Returns a paginated envelope; each brief carries `is_ad_tag` and `is_vast` flags, an absolute screenshot URL plus report_url (auth dashboard) and public_report_url (shareable) deep-links \u2014 link users with those, never hand-build URLs. Repeated scans look like near-duplicate rows: `repeat_index` / `repeat_total` place each one inside its repeat group (0-based) and a non-null `repeat_session_id` means the group ran in `shared` mode (one browser, one IP, cookies carried across repeats) \u2014 group by it to compare the repeats of one combination. `retry_attempt` / `retry_max_attempts` count technical re-crawls of that same scan, not extra scans. `repeat_scan_ids` is not part of this response; use `get_scan` for a single scan's full detail.",
4905
5019
  annotations: {
4906
5020
  title: "List Scans",
4907
5021
  readOnlyHint: true,
@@ -5535,5 +5649,5 @@ function formatToolError(error) {
5535
5649
  }
5536
5650
 
5537
5651
  export { BearerToken, SERVER_INSTRUCTIONS, createHttpApiGateway, createPinoLogger, declareEmptyResourcesAndPrompts, newRequestId, wireToolsIntoMcpServer };
5538
- //# sourceMappingURL=chunk-R56UDCOA.js.map
5539
- //# sourceMappingURL=chunk-R56UDCOA.js.map
5652
+ //# sourceMappingURL=chunk-XCAKB2QW.js.map
5653
+ //# sourceMappingURL=chunk-XCAKB2QW.js.map