@kaminari-ad/mcp 0.10.0 → 0.12.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/CHANGELOG.md CHANGED
@@ -7,6 +7,97 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.0] - 2026-08-07
11
+
12
+ ### Security
13
+
14
+ - **Bump `undici` 8.5.0 to 8.10.0** to clear the npm-audit gate after the
15
+ upstream disclosure of one high-severity advisory
16
+ (GHSA-4cwx-7wf7-3272 — a malformed `private` Cache-Control directive
17
+ makes a shared cache store a per-user response and serve it to other
18
+ users) and four medium ones (GHSA-jr45-8vmc-qm54,
19
+ GHSA-m8rv-5g2x-5cg5, GHSA-8xcm-r25x-g524, GHSA-v3r7-h72x-cjcm). All
20
+ five are fixed in 8.9.0. `undici` is a direct dependency.
21
+
22
+ ### Changed
23
+
24
+ - **Requires api >= v1.33.0 for the webhook tools.** The regenerated zod
25
+ schemas make `failing_since` and `paused_until` required on
26
+ `EndpointHealthResponse`, and the webhook parser picks the whole nested
27
+ `health` object, so against an older API every webhook tool
28
+ (`list_webhooks`, `get_webhook`, `create_webhook`, `update_webhook`,
29
+ `rotate_webhook_secret`) fails with
30
+ `upstream: malformed webhook: health.failing_since: Invalid input`.
31
+ - **README tool list matches the registry again.** It documented 83 tools
32
+ and had drifted 15 behind: the custom-taxonomy domain, the three
33
+ screenshot fetchers, `get_invoice_pdf`, `list_scan_children`,
34
+ `create_custom_role`, and the two account-label tools were all
35
+ registered but undocumented — and the screenshot fetchers and invoice
36
+ PDF were still listed as deliberately not exposed.
37
+ - **`get_alert_stats` counts all time.** The API used to count only the last
38
+ 30 days when no dates were given, while `list_alerts` listed all time, so
39
+ the four buckets never summed to the list's `total`. The API now defaults
40
+ both to all time; the tool description says so, so a model comparing the
41
+ two no longer has to guess which window it got. Requires the API deploy
42
+ that ships the new default — against an older API the counts still cover
43
+ 30 days.
44
+ - **Regenerated `/api/v1` types and zod schemas.** Picks up the new alert
45
+ filters (`policy_set_id`, `tag`, `country_code`, `date_from`, `date_to`,
46
+ `timezone` on list + stats, and the matching `filter_*` fields on
47
+ bulk-status), `policy_set_name` on `AlertResponse`, plus drift that had
48
+ accumulated since the last regen: `last_run_at` on campaign groups,
49
+ campaign/group date filters, and `failing_since` / `paused_until` on
50
+ webhook endpoint health.
51
+
52
+ ## [0.11.0] - 2026-07-28
53
+
54
+ ### Added
55
+
56
+ - **Combo-rule match scope.** `create_custom_rule`, `update_custom_rule`, and
57
+ `test_custom_rule` now document the rule-level `config.match_scope` key used
58
+ by `rule_type='combo'`: `"scan"` (the default, and the behaviour when the key
59
+ is absent) counts conditions across the union of all tags on the scan, while
60
+ `"url"` requires every condition to be satisfied by tags on the same link and
61
+ assigns the output tag to that link. The description warns about the traps a
62
+ per-link rule falls into: a combo built only from link-less tags (AI verdicts,
63
+ crawler behaviour) has no link to attach to and never matches, AI verdicts and
64
+ per-link detections are not evaluated together today, and a per-link rule needs
65
+ at least one positive condition. Any other `match_scope` value is rejected
66
+ before the request is sent, so a misspelled scope surfaces as a tool-input
67
+ error instead of a round trip; the API validates `match_scope` on create,
68
+ update, and the rule-test preview as well, so this is early feedback rather
69
+ than the durable guarantee. The rest of a combo config is still validated only
70
+ for admin-authored system rules, which is why the per-link traps are documented
71
+ rather than enforced. `get_custom_rule` and `list_custom_rules` now flag that
72
+ the returned `config` may carry `match_scope` and that it must be resent
73
+ verbatim, since `update_custom_rule` replaces `config` wholesale.
74
+ - **`referrer` on scan and campaign creation.** `create_scan`,
75
+ `create_bulk_scans`, `create_campaign`, and `update_campaign` accept an
76
+ optional http(s) page URL the check is performed from. With `ad_tag` or
77
+ `vast_tag` it is the publisher page the tag is embedded in: the browser
78
+ commits the page document on that URL without fetching the publisher, so the
79
+ creative renders as if embedded there. With `url` or `ad_discovery` it is
80
+ where the visitor came from, sent as the `Referer` of the page request.
81
+ `update_campaign` also accepts `referrer: null` to clear a stored one.
82
+ Scan and campaign responses surface `referrer` too, so an agent can confirm
83
+ what a scan actually ran with.
84
+
85
+ **Release ordering — this depends on an unreleased API.** The snapshots were
86
+ generated from api `feat/scan-referrer`
87
+ ([api!341](https://gitlab.sdev.pw/adverif/api/-/merge_requests/341)). API
88
+ request DTOs are `extra="ignore"`, so an API without the field drops
89
+ `referrer` silently: no 422, the scan runs with no referrer, and the response
90
+ carries no `referrer` key — the agent gets a green result for a check it
91
+ believes ran from a publisher page. **Do not cut the npm tag or the
92
+ `kaminariad-mcp` image until api!341 is merged and deployed to
93
+ `kaminariadprod1`.** Nothing automated enforces this.
94
+
95
+ ### Changed
96
+
97
+ - Regenerated the OpenAPI type + zod-schema snapshots so the `/api/v1` surface
98
+ they describe carries `referrer`. That field is their only delta versus the
99
+ 0.10.0 snapshots.
100
+
10
101
  ## [0.10.0] - 2026-07-28
11
102
 
12
103
  ### Added
@@ -934,7 +1025,16 @@ Initial public release. The first version that ships to npm under
934
1025
  need them.
935
1026
  - Invoice PDF fetcher — same reason.
936
1027
 
937
- [Unreleased]: https://github.com/kaminari-ad/mcp/compare/v0.2.1...HEAD
1028
+ [Unreleased]: https://github.com/kaminari-ad/mcp/compare/v0.12.0...HEAD
1029
+ [0.12.0]: https://github.com/kaminari-ad/mcp/compare/v0.11.0...v0.12.0
1030
+ [0.11.0]: https://github.com/kaminari-ad/mcp/compare/v0.10.0...v0.11.0
1031
+ [0.10.0]: https://github.com/kaminari-ad/mcp/compare/v0.8.0...v0.10.0
1032
+ [0.8.0]: https://github.com/kaminari-ad/mcp/compare/v0.6.0...v0.8.0
1033
+ [0.6.0]: https://github.com/kaminari-ad/mcp/compare/v0.5.2...v0.6.0
1034
+ [0.5.2]: https://github.com/kaminari-ad/mcp/compare/v0.5.0...v0.5.2
1035
+ [0.5.0]: https://github.com/kaminari-ad/mcp/compare/v0.3.0...v0.5.0
1036
+ [0.3.0]: https://github.com/kaminari-ad/mcp/compare/v0.2.2...v0.3.0
1037
+ [0.2.2]: https://github.com/kaminari-ad/mcp/compare/v0.2.1...v0.2.2
938
1038
  [0.2.1]: https://github.com/kaminari-ad/mcp/compare/v0.2.0...v0.2.1
939
1039
  [0.2.0]: https://github.com/kaminari-ad/mcp/compare/v0.1.5...v0.2.0
940
1040
  [0.1.5]: https://github.com/kaminari-ad/mcp/compare/v0.1.0...v0.1.5
package/README.md CHANGED
@@ -63,7 +63,7 @@ Add to your MCP client config (Cursor: `~/.cursor/mcp.json`; Claude Desktop: `~/
63
63
  }
64
64
  ```
65
65
 
66
- Restart your client. You should see `kaminari-ad` in the MCP servers list with 83 tools exposed.
66
+ Restart your client. You should see `kaminari-ad` in the MCP servers list with 98 tools exposed.
67
67
 
68
68
  ### 2b. Hosted HTTP transport (no install)
69
69
 
@@ -113,24 +113,27 @@ which decides which credential type minted it.
113
113
 
114
114
  ## Tools
115
115
 
116
- 83 tools mirroring most of the public `/api/v1` surface of Kaminari Ad. Every tool carries MCP behaviour annotations (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) so MCP clients can warn before destructive actions. Highlights:
116
+ 98 tools mirroring most of the public `/api/v1` surface of Kaminari Ad. Every tool carries MCP behaviour annotations (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) so MCP clients can warn before destructive actions. The complete list, by domain:
117
117
 
118
- - **Account** (11) — `get_account`, `update_org`, `list_org_users`, `invite_user`, `update_user_role`, `remove_user`, `transfer_ownership`, `list_org_roles`, `list_api_keys`, `create_api_key`, `revoke_api_key`
119
- - **Scans** (6) — `list_scans`, `get_scan`, `create_scan`, `create_bulk_scans`, `recheck_scans`, `cancel_scan`
118
+ - **Account** (14) — `get_account`, `update_org`, `list_org_users`, `invite_user`, `update_user_role`, `remove_user`, `transfer_ownership`, `list_org_roles`, `create_custom_role`, `list_account_labels`, `update_account_labels`, `list_api_keys`, `create_api_key`, `revoke_api_key`
119
+ - **Scans** (10) — `list_scans`, `get_scan`, `list_scan_children`, `create_scan`, `create_bulk_scans`, `recheck_scans`, `cancel_scan`, `get_scan_screenshot`, `get_scan_creative_screenshot`, `get_scan_landing_screenshot`
120
120
  - **Campaigns** (10) — `list_campaigns`, `list_campaigns_picker`, `get_campaign`, `create_campaign`, `update_campaign`, `archive_campaign`, `unarchive_campaign`, `cancel_campaign`, `run_campaign`, `list_campaign_runs`
121
121
  - **Campaign groups** (10) — list/get/create/update/run/cancel/archive/unarchive + `pause_campaign_group_schedule`, `resume_campaign_group_schedule`
122
122
  - **Runs** (3) — `get_run`, `list_run_scans`, `cancel_run` (use `list_campaign_runs` to enumerate runs of a campaign — the API has no standalone `/runs` index)
123
123
  - **Tags** (5) — `list_tags`, `get_tag_definition`, `update_tag_definition`, `delete_tag_definition`, `list_scan_tags`
124
124
  - **Custom rules** (6) — `list_custom_rules`, `get_custom_rule`, `create_custom_rule`, `update_custom_rule`, `delete_custom_rule`, `test_custom_rule`
125
+ - **Custom taxonomies** (7) — `list_custom_taxonomies`, `get_custom_taxonomy`, `create_custom_taxonomy`, `update_custom_taxonomy`, `delete_custom_taxonomy`, `restore_custom_taxonomy`, `parse_custom_taxonomy_text`
125
126
  - **Policy sets** (6) — `list_policy_sets`, `get_policy_set`, `create_policy_set`, `update_policy_set`, `delete_policy_set`, `request_policy_set_approval`
126
127
  - **Alerts** (3) — `list_alerts`, `update_alert_status`, `get_alert_stats`
127
128
  - **Webhooks** (11) — `list_webhooks`, `get_webhook`, `create_webhook`, `update_webhook`, `delete_webhook`, `list_webhook_event_types`, `list_webhook_deliveries`, `test_webhook`, `rotate_webhook_secret`, `replay_webhook_delivery`, `bulk_replay_webhook`
128
129
  - **Billing** (4) — `get_billing_summary`, `list_usage`, `get_usage_summary`, `list_balance_history`
129
- - **Invoicing** (1) — `list_invoices`
130
+ - **Invoicing** (2) — `list_invoices`, `get_invoice_pdf`
130
131
  - **Alert notifications** (5) — `list_alert_destinations`, `delete_alert_destination`, `set_alert_destination_version`, `get_campaign_alert_overrides`, `set_campaign_alert_overrides`
131
132
  - **Reference data** (2) — `list_geos`, `list_emulators`
132
133
 
133
- Not exposed (intentionally): binary scan-screenshot fetchers, invoice PDF, and the public marketing forms (`/contact`, `/demo-inquiries`). Open an issue if you need one of those.
134
+ Screenshots (`get_scan_screenshot`, `get_scan_creative_screenshot`, `get_scan_landing_screenshot`) come back as inline MCP `image` blocks and `get_invoice_pdf` as an inline resource block no second fetch, no presigned URL.
135
+
136
+ Not exposed (intentionally): the raw creative artifacts (`creative-html`, `creative-video`, `vast-xml`) and the public marketing forms (`/contact`, `/demo-inquiries`). Open an issue if you need one of those.
134
137
 
135
138
  ## Example agent prompts
136
139
 
package/dist/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { NAME, VERSION, err, ok } from './chunk-KFVVS55N.js';
2
+ import { NAME, VERSION, err, ok } from './chunk-TMUGAKI2.js';
3
3
  import process from 'process';
4
4
  import { z } from 'zod';
5
5
 
@@ -160,10 +160,10 @@ async function main() {
160
160
  }
161
161
  const config = configResult.value;
162
162
  if (config.transport === "stdio") {
163
- const { bootstrapStdio } = await import('./stdio-bootstrap-7EXISZ66.js');
163
+ const { bootstrapStdio } = await import('./stdio-bootstrap-JW6I4H3U.js');
164
164
  return bootstrapStdio(config);
165
165
  }
166
- const { bootstrapHttp } = await import('./http-bootstrap-3MGAQHND.js');
166
+ const { bootstrapHttp } = await import('./http-bootstrap-Y4YKT7KV.js');
167
167
  return bootstrapHttp(config);
168
168
  }
169
169
  main().then(
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { err, ok } from './chunk-KFVVS55N.js';
2
+ import { err, ok } from './chunk-TMUGAKI2.js';
3
3
  import { createHash, randomUUID } from 'crypto';
4
4
  import createClient from 'openapi-fetch';
5
5
  import { fetch } from 'undici';
@@ -245,6 +245,7 @@ z.object({
245
245
  url: z.union([z.string(), z.null()]).optional(),
246
246
  ad_tag: z.union([z.string(), z.null()]).optional(),
247
247
  vast_tag: z.union([z.string(), z.null()]).optional(),
248
+ referrer: z.union([z.string(), z.null()]).optional(),
248
249
  country_code: z.string().min(2).max(2),
249
250
  emulator_id: z.string().min(1).max(100),
250
251
  proxy: ProxyTargetRequest.optional(),
@@ -362,6 +363,7 @@ var ScanResponse = z.object({
362
363
  public_report_url: z.string().optional().default(""),
363
364
  ad_tag: z.union([z.string(), z.null()]).optional(),
364
365
  vast_tag: z.union([z.string(), z.null()]).optional(),
366
+ referrer: z.union([z.string(), z.null()]).optional(),
365
367
  creative_kind: z.enum(["banner", "video"]).optional().default("banner"),
366
368
  creative_screenshot_url: z.string().optional().default(""),
367
369
  creative_video_url: z.string().optional().default(""),
@@ -433,6 +435,7 @@ z.object({
433
435
  url: z.union([z.string(), z.null()]).optional(),
434
436
  ad_tag: z.union([z.string(), z.null()]).optional(),
435
437
  vast_tag: z.union([z.string(), z.null()]).optional(),
438
+ referrer: z.union([z.string(), z.null()]).optional(),
436
439
  country_codes: z.array(z.string()).min(1),
437
440
  emulator_id: z.string().min(1).max(100),
438
441
  proxy: ProxyTargetRequest.optional(),
@@ -468,7 +471,8 @@ var CampaignGroupResponse = z.object({
468
471
  is_archived: z.boolean(),
469
472
  schedule_paused: z.boolean(),
470
473
  created_at: z.string().datetime({ offset: true }),
471
- campaign_count: z.union([z.number(), z.null()]).optional()
474
+ campaign_count: z.union([z.number(), z.null()]).optional(),
475
+ last_run_at: z.union([z.string(), z.null()]).optional()
472
476
  }).passthrough();
473
477
  z.object({ name: z.union([z.string(), z.null()]) }).partial().passthrough();
474
478
  var BulkCampaignFailure = z.object({
@@ -490,6 +494,7 @@ z.object({
490
494
  url: z.union([z.string(), z.null()]).optional(),
491
495
  ad_tag: z.union([z.string(), z.null()]).optional(),
492
496
  vast_tag: z.union([z.string(), z.null()]).optional(),
497
+ referrer: z.union([z.string(), z.null()]).optional(),
493
498
  country_codes: z.array(z.string()).min(1),
494
499
  group_id: z.union([z.string(), z.null()]).optional(),
495
500
  emulator_categories: z.array(z.string()).optional(),
@@ -522,6 +527,7 @@ var CampaignResponse = z.object({
522
527
  url: z.string(),
523
528
  ad_tag: z.union([z.string(), z.null()]).optional(),
524
529
  vast_tag: z.union([z.string(), z.null()]).optional(),
530
+ referrer: z.union([z.string(), z.null()]).optional(),
525
531
  country_codes: z.array(z.string()),
526
532
  group_id: z.string().uuid(),
527
533
  emulator_selection: EmulatorSelectionResponse,
@@ -562,6 +568,7 @@ z.object({
562
568
  url: z.union([z.string(), z.null()]),
563
569
  ad_tag: z.union([z.string(), z.null()]),
564
570
  vast_tag: z.union([z.string(), z.null()]),
571
+ referrer: z.union([z.string(), z.null()]),
565
572
  country_codes: z.union([z.array(z.string()), z.null()]),
566
573
  group_id: z.union([z.string(), z.null()]),
567
574
  emulator_categories: z.union([z.array(z.string()), z.null()]),
@@ -861,6 +868,7 @@ var AlertResponse = z.object({
861
868
  scan_url: z.string(),
862
869
  offer_url: z.string(),
863
870
  tag_display_name: z.string(),
871
+ policy_set_name: z.union([z.string(), z.null()]).optional(),
864
872
  rule_type: z.string().optional().default("tag"),
865
873
  matched_value: z.union([z.string(), z.null()]).optional()
866
874
  }).passthrough();
@@ -877,7 +885,13 @@ z.object({
877
885
  ids: z.union([z.array(z.string().uuid()), z.null()]).optional(),
878
886
  all_matching: z.boolean().optional().default(false),
879
887
  filter_status: z.union([AlertStatus, z.null()]).optional(),
880
- filter_campaign_id: z.union([z.string(), z.null()]).optional()
888
+ filter_campaign_id: z.union([z.string(), z.null()]).optional(),
889
+ filter_policy_set_ids: z.union([z.array(z.string().uuid()), z.null()]).optional(),
890
+ filter_tag_slugs: z.union([z.array(z.string()), z.null()]).optional(),
891
+ filter_country_codes: z.union([z.array(z.string()), z.null()]).optional(),
892
+ filter_date_from: z.union([z.string(), z.null()]).optional(),
893
+ filter_date_to: z.union([z.string(), z.null()]).optional(),
894
+ filter_timezone: z.union([z.string(), z.null()]).optional()
881
895
  }).passthrough();
882
896
  z.object({ updated: z.number().int(), skipped: z.number().int() }).passthrough();
883
897
  var AlertStatsResponse = z.object({
@@ -968,7 +982,9 @@ var EndpointHealthResponse = z.object({
968
982
  consecutive_failures: z.number().int(),
969
983
  last_delivery_at: z.union([z.string(), z.null()]),
970
984
  last_delivery_status: z.union([z.number(), z.null()]),
971
- success_rate_7d: z.number()
985
+ success_rate_7d: z.number(),
986
+ failing_since: z.union([z.string(), z.null()]),
987
+ paused_until: z.union([z.string(), z.null()])
972
988
  }).passthrough();
973
989
  var WebhookResponse = z.object({
974
990
  id: z.string().uuid(),
@@ -1339,6 +1355,7 @@ var CampaignSchema = schemas.CampaignResponse.pick({
1339
1355
  url: true,
1340
1356
  ad_tag: true,
1341
1357
  vast_tag: true,
1358
+ referrer: true,
1342
1359
  country_codes: true,
1343
1360
  group_id: true,
1344
1361
  emulator_selection: true,
@@ -1772,6 +1789,7 @@ var ScanSchema = schemas.ScanResponse.pick({
1772
1789
  public_report_url: true,
1773
1790
  ad_tag: true,
1774
1791
  vast_tag: true,
1792
+ referrer: true,
1775
1793
  creative_kind: true,
1776
1794
  video: true,
1777
1795
  creative_screenshot_url: true,
@@ -3179,7 +3197,7 @@ var setCampaignAlertOverridesTool = {
3179
3197
  var GetAlertStatsInputShape = {};
3180
3198
  var getAlertStatsTool = {
3181
3199
  name: "get_alert_stats",
3182
- description: "Get alert counts grouped by status: open, escalated, resolved, dismissed (total is implicit in the sum). Status names match the canonical AlertStatus enum used by `list_alerts` and `update_alert_status`.",
3200
+ description: "Get alert counts grouped by status: open, escalated, resolved, dismissed. Counts cover the caller's organization over all time and are not filtered, so the four sum to the `total` an unfiltered `list_alerts` reports. Status names match the canonical AlertStatus enum used by `list_alerts` and `update_alert_status`.",
3183
3201
  annotations: {
3184
3202
  title: "Get Alert Stats",
3185
3203
  readOnlyHint: true,
@@ -3672,6 +3690,10 @@ function pickCampaignConfigBody(input) {
3672
3690
  if (input.schedule_timezone !== void 0) body.schedule_timezone = input.schedule_timezone;
3673
3691
  return body;
3674
3692
  }
3693
+ var campaignReferrerUrl = z.string().url().regex(/^https?:\/\//i, "Must be an http(s) URL.").max(2048);
3694
+ 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.";
3695
+ var campaignReferrerField = campaignReferrerUrl.optional().describe(CAMPAIGN_REFERRER_DESCRIPTION);
3696
+ var campaignReferrerUpdateField = campaignReferrerUrl.nullable().optional().describe(`${CAMPAIGN_REFERRER_DESCRIPTION} Pass null to clear it.`);
3675
3697
 
3676
3698
  // src/application/tools/campaigns/create-campaign.tool.ts
3677
3699
  var CreateCampaignInputShape = {
@@ -3686,6 +3708,7 @@ var CreateCampaignInputShape = {
3686
3708
  vast_tag: z.string().optional().describe(
3687
3709
  "VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (required if campaign_type=vast)."
3688
3710
  ),
3711
+ referrer: campaignReferrerField,
3689
3712
  country_codes: z.array(z.string().length(2)).min(1).describe("ISO 3166-1 alpha-2 codes \u2014 one scan per country per run."),
3690
3713
  group_id: z.string().uuid().optional().describe("Parent group UUID; defaults to the org's default group."),
3691
3714
  ...campaignConfigFields,
@@ -3712,6 +3735,7 @@ var createCampaignTool = {
3712
3735
  ...input.url !== void 0 ? { url: input.url } : {},
3713
3736
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
3714
3737
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
3738
+ ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
3715
3739
  ...input.group_id !== void 0 ? { group_id: input.group_id } : {},
3716
3740
  ...input.labels !== void 0 ? { labels: input.labels } : {},
3717
3741
  ...input.policy_set_id !== void 0 ? { policy_set_id: input.policy_set_id } : {},
@@ -3882,6 +3906,7 @@ var UpdateCampaignInputShape = {
3882
3906
  vast_tag: z.string().optional().describe(
3883
3907
  "New VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (vast-type campaigns)."
3884
3908
  ),
3909
+ referrer: campaignReferrerUpdateField,
3885
3910
  country_codes: z.array(z.string().length(2)).optional().describe("Replace the country list."),
3886
3911
  group_id: z.string().uuid().optional().describe("Move the campaign to another group."),
3887
3912
  ...campaignConfigFields,
@@ -3891,7 +3916,7 @@ var UpdateCampaignInputShape = {
3891
3916
  };
3892
3917
  var updateCampaignTool = {
3893
3918
  name: "update_campaign",
3894
- description: "Update one or more fields of a campaign. Fields not supplied are left unchanged. `policy_set_id` accepts null to clear the binding.",
3919
+ 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.",
3895
3920
  annotations: {
3896
3921
  title: "Update Campaign",
3897
3922
  readOnlyHint: false,
@@ -3906,6 +3931,7 @@ var updateCampaignTool = {
3906
3931
  ...input.url !== void 0 ? { url: input.url } : {},
3907
3932
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
3908
3933
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
3934
+ ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
3909
3935
  ...input.country_codes !== void 0 ? { country_codes: input.country_codes } : {},
3910
3936
  ...input.group_id !== void 0 ? { group_id: input.group_id } : {},
3911
3937
  ...input.labels !== void 0 ? { labels: input.labels } : {},
@@ -3918,6 +3944,21 @@ var updateCampaignTool = {
3918
3944
  return ok(result.value);
3919
3945
  }
3920
3946
  };
3947
+ 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.';
3948
+ 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.';
3949
+ var ruleConfigField = z.record(z.unknown()).superRefine((config, ctx) => {
3950
+ const scope = config["match_scope"];
3951
+ if (scope === void 0 || scope === "scan" || scope === "url") {
3952
+ return;
3953
+ }
3954
+ ctx.addIssue({
3955
+ code: z.ZodIssueCode.custom,
3956
+ path: ["match_scope"],
3957
+ message: 'config.match_scope must be "scan" or "url". Omit the key for scan-wide matching (the default).'
3958
+ });
3959
+ });
3960
+
3961
+ // src/application/tools/custom-rules/create-custom-rule.tool.ts
3921
3962
  var CreateCustomRuleInputShape = {
3922
3963
  name: z.string().min(1).max(200).describe(
3923
3964
  "Display name. For non-LLM rules this also becomes the `display_name` of the auto-registered tag definition (see `tag_slug`)."
@@ -3928,8 +3969,8 @@ var CreateCustomRuleInputShape = {
3928
3969
  rule_type: z.string().max(50).describe(
3929
3970
  "Rule engine. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `blacklist_domain`, `combo`, `llm`. The API validates."
3930
3971
  ),
3931
- config: z.record(z.unknown()).describe(
3932
- "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`)."
3972
+ config: ruleConfigField.describe(
3973
+ "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
3933
3974
  ),
3934
3975
  target: z.string().max(30).optional().describe(
3935
3976
  "Where to apply the rule (e.g. 'page' for landing HTML). Default: page. See API docs for the full set of valid values."
@@ -3937,7 +3978,7 @@ var CreateCustomRuleInputShape = {
3937
3978
  };
3938
3979
  var createCustomRuleTool = {
3939
3980
  name: "create_custom_rule",
3940
- 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`.",
3981
+ 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`.",
3941
3982
  annotations: {
3942
3983
  title: "Create Custom Rule",
3943
3984
  readOnlyHint: false,
@@ -3982,7 +4023,7 @@ var deleteCustomRuleTool = {
3982
4023
  var GetCustomRuleInputShape = { rule_id: z.string().uuid().describe("Rule UUID.") };
3983
4024
  var getCustomRuleTool = {
3984
4025
  name: "get_custom_rule",
3985
- description: "Get one custom rule by UUID with name, tag-slug, type, config object, target, active flag.",
4026
+ description: "Get one custom rule by UUID with name, tag-slug, type, config object, target, active flag. " + COMBO_MATCH_SCOPE_READ_DOC,
3986
4027
  annotations: {
3987
4028
  title: "Get Custom Rule",
3988
4029
  readOnlyHint: true,
@@ -4003,7 +4044,7 @@ var ListCustomRulesInputShape = {
4003
4044
  };
4004
4045
  var listCustomRulesTool = {
4005
4046
  name: "list_custom_rules",
4006
- 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.",
4047
+ 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,
4007
4048
  annotations: {
4008
4049
  title: "List Custom Rules",
4009
4050
  readOnlyHint: true,
@@ -4022,8 +4063,8 @@ var TestCustomRuleInputShape = {
4022
4063
  rule_type: z.string().max(50).describe(
4023
4064
  "Rule engine type. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `blacklist_domain`, `combo`, `llm`."
4024
4065
  ),
4025
- config: z.record(z.unknown()).describe(
4026
- "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`."
4066
+ config: ruleConfigField.describe(
4067
+ "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
4027
4068
  ),
4028
4069
  target: z.string().max(30).describe(
4029
4070
  "Where to apply the rule (e.g. 'page' for landing HTML). See API docs for the full set of valid values."
@@ -4032,7 +4073,7 @@ var TestCustomRuleInputShape = {
4032
4073
  };
4033
4074
  var testCustomRuleTool = {
4034
4075
  name: "test_custom_rule",
4035
- 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`.",
4076
+ 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`.",
4036
4077
  annotations: {
4037
4078
  title: "Test Custom Rule",
4038
4079
  readOnlyHint: true,
@@ -4060,8 +4101,8 @@ var UpdateCustomRuleInputShape = {
4060
4101
  tag_slug: z.string().max(100).optional().describe(
4061
4102
  "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`."
4062
4103
  ),
4063
- config: z.record(z.unknown()).optional().describe(
4064
- "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."
4104
+ config: ruleConfigField.optional().describe(
4105
+ "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
4065
4106
  ),
4066
4107
  target: z.string().max(30).optional().describe(
4067
4108
  "Where to apply the rule (e.g. 'page' for landing HTML). See API docs for the full set of valid values."
@@ -4070,7 +4111,7 @@ var UpdateCustomRuleInputShape = {
4070
4111
  };
4071
4112
  var updateCustomRuleTool = {
4072
4113
  name: "update_custom_rule",
4073
- 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.)",
4114
+ 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.)",
4074
4115
  annotations: {
4075
4116
  title: "Update Custom Rule",
4076
4117
  readOnlyHint: false,
@@ -4733,6 +4774,9 @@ var scanProxyField = z.object({
4733
4774
  city: z.string().optional().describe("Proxy city targeting."),
4734
4775
  isp: z.string().optional().describe("Proxy ISP targeting.")
4735
4776
  }).optional().describe("Optional proxy geo targeting for the crawl. Omit to use the org default.");
4777
+ var scanReferrerField = z.string().url().regex(/^https?:\/\//i, "Must be an http(s) URL.").max(2048).optional().describe(
4778
+ "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."
4779
+ );
4736
4780
 
4737
4781
  // src/application/tools/scans/create-bulk-scans.tool.ts
4738
4782
  var CreateBulkScansInputShape = {
@@ -4743,6 +4787,7 @@ var CreateBulkScansInputShape = {
4743
4787
  vast_tag: z.string().optional().describe(
4744
4788
  "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`."
4745
4789
  ),
4790
+ referrer: scanReferrerField,
4746
4791
  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."),
4747
4792
  emulator_id: z.string().min(1).max(100).describe("Device/OS profile slug; same for every country in the batch."),
4748
4793
  proxy: scanProxyField,
@@ -4767,6 +4812,7 @@ var createBulkScansTool = {
4767
4812
  ...input.url !== void 0 ? { url: input.url } : {},
4768
4813
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
4769
4814
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
4815
+ ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
4770
4816
  ...input.proxy !== void 0 ? { proxy: input.proxy } : {},
4771
4817
  ...input.labels !== void 0 ? { labels: input.labels } : {},
4772
4818
  ...pickRepeatRetryBody(input)
@@ -4786,6 +4832,7 @@ var CreateScanInputShape = {
4786
4832
  vast_tag: z.string().optional().describe(
4787
4833
  "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`."
4788
4834
  ),
4835
+ referrer: scanReferrerField,
4789
4836
  country_code: z.string().length(2).describe("ISO 3166-1 alpha-2 country code, e.g. US, DE, JP. Determines proxy geo."),
4790
4837
  emulator_id: z.string().min(1).max(100).describe("Device/OS profile slug; use `list_emulators` to discover valid values."),
4791
4838
  proxy: scanProxyField,
@@ -4815,6 +4862,7 @@ var createScanTool = {
4815
4862
  ...input.url !== void 0 ? { url: input.url } : {},
4816
4863
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
4817
4864
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
4865
+ ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
4818
4866
  ...input.proxy !== void 0 ? { proxy: input.proxy } : {},
4819
4867
  ...input.labels !== void 0 ? { labels: input.labels } : {},
4820
4868
  ...input.campaign_id !== void 0 ? { campaign_id: input.campaign_id } : {},
@@ -5611,5 +5659,5 @@ function formatToolError(error) {
5611
5659
  }
5612
5660
 
5613
5661
  export { BearerToken, SERVER_INSTRUCTIONS, createHttpApiGateway, createPinoLogger, declareEmptyResourcesAndPrompts, newRequestId, wireToolsIntoMcpServer };
5614
- //# sourceMappingURL=chunk-5UVZYMC3.js.map
5615
- //# sourceMappingURL=chunk-5UVZYMC3.js.map
5662
+ //# sourceMappingURL=chunk-T6GEYHLP.js.map
5663
+ //# sourceMappingURL=chunk-T6GEYHLP.js.map