@kaminari-ad/mcp 0.19.1 → 0.23.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,102 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.23.0] - 2026-09-29
11
+
12
+ > Requires the API-side ad-format deploy. Until it lands, production
13
+ > silently ignores `ad_formats` as an unknown body field: a `['pop']` request
14
+ > checks and bills every format and reads back without the field. Do not tag
15
+ > this release ahead of the API.
16
+
17
+ ### Added
18
+
19
+ - **`ad_formats` on `create_scan`, `create_campaign` and `update_campaign`.**
20
+ An ad-discovery run can now be narrowed to some formats: any of
21
+ `banner`, `video` and `pop` (pop-unders and tab-unders). A format left out
22
+ is neither captured nor billed, so `['pop']` checks a publisher's pops
23
+ without screenshotting or clicking every banner on the page. Omit the
24
+ field to check every format; on `update_campaign`, pass `null` to go back
25
+ to every format.
26
+
27
+ `ScanResponse` and `CampaignResponse` echo the selection (`null` for
28
+ every format). The read side is parsed as plain strings rather than the
29
+ request enum, so a format added on the API side cannot fail a whole
30
+ `get_scan` / `get_campaign` call. The generated schemas were left alone:
31
+ the field is declared through the port's escape hatch until the next
32
+ regen against the deployed API, like `max_discovered_ads` before it.
33
+
34
+ ### Security
35
+
36
+ - `undici` 8.10.0 → 8.11.2 (GHSA-3wwx-pv8p-q78v, WebSocket
37
+ permessage-deflate DoS) and transitive `ip-address` 10.4.0 → 10.7.2
38
+ (link-local and NAT64 classification bypasses), which were failing
39
+ `audit:deps`.
40
+
41
+ ## [0.22.0] - 2026-09-15
42
+
43
+ ### Changed
44
+
45
+ - **`regexp_request_url` and `regexp_request_body` accept the creative
46
+ surface.** Both were rejected locally with anything but `target='page'`,
47
+ which made the rule that catches an exploit kit embedded in a banner
48
+ unauthorable: that kit is a script inside the creative's own iframe and
49
+ appears on no landing page's request list. Both now take `'page'`,
50
+ `'creative'` or `'creative_and_page'` (evaluated on each surface, every
51
+ match reporting the one it came from), matching what the API validates.
52
+ Tool descriptions updated with it, since an agent picks the target from
53
+ them.
54
+
55
+ Requires the API-side deploy that widens the same validator. Until it
56
+ lands, production rejects the two new targets with HTTP 422.
57
+
58
+ ## [0.21.0] - 2026-09-14
59
+
60
+ > Requires the API-side KAMIAD-177 deploy. Until it lands, production
61
+ > rejects `ignore_first_n_domains` as an unknown body field, so do not tag
62
+ > this release ahead of the API.
63
+
64
+ ### Added
65
+
66
+ - **`ignore_first_n_domains` on `create_campaign`, `update_campaign`,
67
+ `create_scan` and `create_bulk_scans`.** A check that enters through the
68
+ caller's own click or tracking domains used to have those domains rated
69
+ by every domain checker, so a reputation hit on the caller's own
70
+ infrastructure tagged the material and raised an alert that said nothing
71
+ about the offer. The new field (0-5, default 0) drops the first N unique
72
+ domains of the redirect chain, counted from the entry point, from
73
+ detection and tagging. Domains fold to their registrable form, so
74
+ `www.example.com` and `example.com` consume one slot between them, and
75
+ sub-requests made from a skipped domain are excluded too.
76
+
77
+ Deliberately NOT nullable, unlike `referrer` and `max_discovered_ads`
78
+ next to it: `0` is both the default and the reset, so a nullable variant
79
+ would only offer an agent a second spelling the API answers with a 422.
80
+ The shared `.describe()` text warns that a skipped domain is checked by
81
+ nothing, because the failure mode of guessing high is a hidden finding
82
+ rather than an error.
83
+
84
+ `CampaignResponse` and `ScanResponse` both echo the field, so an agent can
85
+ read back what a campaign will do and how much of a finished scan's chain
86
+ was excluded — the latter is the only way to answer that once the campaign
87
+ behind the scan has been edited. Parsed as optional rather than defaulted
88
+ to 0: an api that predates the field says nothing, and inventing a 0 would
89
+ claim the scan skipped nothing when the truth is unknown.
90
+
91
+ The generated schemas were left alone: the field is declared through the
92
+ same port-side intersection `max_discovered_ads` uses, because
93
+ `gen:api-types` reads the DEPLOYED spec and cannot see an unshipped
94
+ field. The next regen absorbs it and the intersections can go.
95
+
96
+ ## [0.20.0] - 2026-09-13
97
+
98
+ ### Added
99
+
100
+ - **`set_default_policy_set`** — mark or clear an owned policy set as
101
+ the organization's default for new campaigns (`POST /api/v1/policy-sets/{id}/set-default`).
102
+ `list_policy_sets` / `get_policy_set` now surface `is_default`. Omit
103
+ `policy_set_id` on `create_campaign` to bind that default; pass
104
+ `null` to create an unbound campaign.
105
+
10
106
  ## [0.19.1] - 2026-09-10
11
107
 
12
108
  > Requires the API-side KAMIAD-104 deploy. Until it lands, production
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 107 tools exposed.
66
+ Restart your client. You should see `kaminari-ad` in the MCP servers list with 108 tools exposed.
67
67
 
68
68
  ### 2b. Hosted HTTP transport (no install)
69
69
 
@@ -113,7 +113,7 @@ which decides which credential type minted it.
113
113
 
114
114
  ## Tools
115
115
 
116
- 107 tools covering 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:
116
+ 108 tools covering 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
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
119
  - **Scans** (13) — `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`, `get_scan_creative_html`, `get_scan_creative_video`, `get_scan_vast_xml`
@@ -123,7 +123,7 @@ which decides which credential type minted it.
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
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`
126
- - **Policy sets** (10) — `list_policy_sets`, `get_policy_set`, `create_policy_set`, `update_policy_set`, `delete_policy_set`, `request_policy_set_approval`, `unpublish_policy_set`, `list_policy_set_campaigns`, `attach_policy_set_campaigns`, `detach_policy_set_campaigns`
126
+ - **Policy sets** (11) — `list_policy_sets`, `get_policy_set`, `create_policy_set`, `update_policy_set`, `delete_policy_set`, `request_policy_set_approval`, `unpublish_policy_set`, `set_default_policy_set`, `list_policy_set_campaigns`, `attach_policy_set_campaigns`, `detach_policy_set_campaigns`
127
127
  - **Alerts** (4) — `list_alerts`, `update_alert_status`, `bulk_update_alert_status`, `get_alert_stats`
128
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`
129
129
  - **Billing** (4) — `get_billing_summary`, `list_usage`, `get_usage_summary`, `list_balance_history`
package/dist/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { NAME, VERSION, err, ok } from './chunk-MKBTS4IH.js';
2
+ import { NAME, VERSION, err, ok } from './chunk-6INS6YMW.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-4PXPX6BD.js');
163
+ const { bootstrapStdio } = await import('./stdio-bootstrap-M7I2WD5F.js');
164
164
  return bootstrapStdio(config);
165
165
  }
166
- const { bootstrapHttp } = await import('./http-bootstrap-FN4DOOJT.js');
166
+ const { bootstrapHttp } = await import('./http-bootstrap-NSZVATPV.js');
167
167
  return bootstrapHttp(config);
168
168
  }
169
169
  main().then(
@@ -3,8 +3,8 @@ export { err, ok } from 'neverthrow';
3
3
 
4
4
  // src/shared/version.ts
5
5
  var NAME = "@kaminari-ad/mcp";
6
- var VERSION = "0.19.1";
6
+ var VERSION = "0.23.0";
7
7
 
8
8
  export { NAME, VERSION };
9
- //# sourceMappingURL=chunk-MKBTS4IH.js.map
10
- //# sourceMappingURL=chunk-MKBTS4IH.js.map
9
+ //# sourceMappingURL=chunk-6INS6YMW.js.map
10
+ //# sourceMappingURL=chunk-6INS6YMW.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shared/version.ts"],"names":[],"mappings":";;;;AASO,IAAM,IAAA,GAAO;AACb,IAAM,OAAA,GAAU","file":"chunk-MKBTS4IH.js","sourcesContent":["/**\n * Package version and name. Hard-coded as constants here, asserted to\n * match `package.json` by a unit test.\n *\n * Why not import `package.json`: it would force JSON-module support at\n * runtime and tsup-bundling would inline the entire manifest. Two\n * constants + one assertion test is simpler and gives the same safety.\n */\n\nexport const NAME = \"@kaminari-ad/mcp\";\nexport const VERSION = \"0.19.1\";\n"]}
1
+ {"version":3,"sources":["../src/shared/version.ts"],"names":[],"mappings":";;;;AASO,IAAM,IAAA,GAAO;AACb,IAAM,OAAA,GAAU","file":"chunk-6INS6YMW.js","sourcesContent":["/**\n * Package version and name. Hard-coded as constants here, asserted to\n * match `package.json` by a unit test.\n *\n * Why not import `package.json`: it would force JSON-module support at\n * runtime and tsup-bundling would inline the entire manifest. Two\n * constants + one assertion test is simpler and gives the same safety.\n */\n\nexport const NAME = \"@kaminari-ad/mcp\";\nexport const VERSION = \"0.23.0\";\n"]}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { err, ok } from './chunk-MKBTS4IH.js';
2
+ import { err, ok } from './chunk-6INS6YMW.js';
3
3
  import { createHash, randomUUID } from 'crypto';
4
4
  import createClient from 'openapi-fetch';
5
5
  import { fetch } from 'undici';
@@ -832,6 +832,7 @@ var PolicySetResponse = z.object({
832
832
  organization_id: z.string().uuid(),
833
833
  visibility: z.string(),
834
834
  is_approved: z.boolean(),
835
+ is_default: z.boolean(),
835
836
  entries: z.array(PolicyEntryResponse),
836
837
  campaigns: z.array(LinkedCampaignResponse).optional(),
837
838
  campaigns_total: z.number().int().optional().default(0),
@@ -846,6 +847,7 @@ var PolicySetListItem = z.object({
846
847
  organization_id: z.string().uuid(),
847
848
  visibility: z.string(),
848
849
  is_approved: z.boolean(),
850
+ is_default: z.boolean(),
849
851
  created_at: z.string().datetime({ offset: true })
850
852
  }).passthrough();
851
853
  z.object({
@@ -873,6 +875,7 @@ z.object({
873
875
  campaign_ids: z.array(z.string().uuid()).max(500),
874
876
  detach_all: z.boolean().default(false)
875
877
  }).partial().passthrough();
878
+ z.object({ is_default: z.boolean() }).passthrough();
876
879
  var AlertStatus = z.enum(["open", "escalated", "resolved", "dismissed"]);
877
880
  z.union([AlertStatus, z.null()]).optional();
878
881
  var AlertResponse = z.object({
@@ -1377,7 +1380,7 @@ var CampaignSchema = schemas.CampaignResponse.pick({
1377
1380
  is_archived: true,
1378
1381
  created_at: true,
1379
1382
  last_run_at: true
1380
- }).extend({ repeat_mode: schemas.RepeatMode.default("isolated") }).extend({ max_discovered_ads: z.number().int().nullable().optional() }).strip();
1383
+ }).extend({ repeat_mode: schemas.RepeatMode.default("isolated") }).extend({ max_discovered_ads: z.number().int().nullable().optional() }).extend({ ad_formats: z.array(z.string()).nullable().optional() }).extend({ ignore_first_n_domains: z.number().int().optional() }).strip();
1381
1384
  var parseCampaign = (raw) => parseWithSchema(CampaignSchema, raw, "campaign");
1382
1385
  var parseCampaignPage = (raw) => parsePagedWithItemSchema(CampaignSchema, raw, "campaigns");
1383
1386
 
@@ -1747,6 +1750,7 @@ var PolicySetSchema = schemas.PolicySetResponse.pick({
1747
1750
  organization_id: true,
1748
1751
  visibility: true,
1749
1752
  is_approved: true,
1753
+ is_default: true,
1750
1754
  entries: true,
1751
1755
  created_at: true
1752
1756
  }).strip();
@@ -1760,6 +1764,7 @@ var PolicySetListItemSchema = schemas.PolicySetListItem.pick({
1760
1764
  organization_id: true,
1761
1765
  visibility: true,
1762
1766
  is_approved: true,
1767
+ is_default: true,
1763
1768
  created_at: true
1764
1769
  }).strip();
1765
1770
  var parsePolicySetPage = (raw) => parsePagedWithItemSchema(PolicySetListItemSchema, raw, "policy-sets");
@@ -1839,7 +1844,7 @@ var ScanSchema = schemas.ScanResponse.pick({
1839
1844
  repeat_scan_ids: true,
1840
1845
  retry_attempt: true,
1841
1846
  retry_max_attempts: true
1842
- }).extend({ creative_kind: z.string().default("banner") }).extend({ max_discovered_ads: z.number().int().nullable().optional() }).extend({ network_host: z.string().optional() }).strip();
1847
+ }).extend({ creative_kind: z.string().default("banner") }).extend({ max_discovered_ads: z.number().int().nullable().optional() }).extend({ ad_formats: z.array(z.string()).nullable().optional() }).extend({ network_host: z.string().optional() }).extend({ ignore_first_n_domains: z.number().int().optional() }).strip();
1843
1848
  var ScanArraySchema = z.array(ScanSchema);
1844
1849
  var parseScan = (raw) => parseWithSchema(ScanSchema, raw, "scan");
1845
1850
  var parseScanArray = (raw) => parseWithSchema(ScanArraySchema, raw, "scans");
@@ -2562,6 +2567,14 @@ function createHttpApiGateway(config) {
2562
2567
  parseEmpty
2563
2568
  );
2564
2569
  },
2570
+ async setDefaultPolicySet(id, isDefault) {
2571
+ return call(
2572
+ "POST",
2573
+ "/api/v1/policy-sets/{policy_set_id}/set-default",
2574
+ { params: { path: { policy_set_id: id } }, body: { is_default: isDefault } },
2575
+ parseEmpty
2576
+ );
2577
+ },
2565
2578
  // ── Custom taxonomies ─────────────────────────────────────────
2566
2579
  async listCustomTaxonomies(filters) {
2567
2580
  return call(
@@ -3890,6 +3903,22 @@ var cancelCampaignTool = {
3890
3903
  return ok(result.value);
3891
3904
  }
3892
3905
  };
3906
+ var AD_FORMATS_DESCRIPTION = "Which ad formats an ad-discovery run checks: any of `banner`, `video` and `pop` (`pop` covers pop-unders and tab-unders), each at most once. A format left out is neither captured nor billed \u2014 `['pop']` checks the page's pops without screenshotting or clicking a single banner. A tab-under the page forces on its own is still reported as a pop. Only valid on an ad-discovery target \u2014 `ad_discovery: true` on a scan, or `campaign_type: \"ad_discovery\"` on a campaign \u2014 and rejected with 422 anywhere else, as is an empty list.";
3907
+ var adFormat = z.enum(["banner", "video", "pop"]);
3908
+ var adFormats = z.array(adFormat).min(1).max(adFormat.options.length);
3909
+ var adFormatsField = adFormats.optional().describe(`${AD_FORMATS_DESCRIPTION} Omit it to check every format.`);
3910
+ var adFormatsUpdateField = adFormats.nullable().optional().describe(
3911
+ `${AD_FORMATS_DESCRIPTION} Omitting the field leaves the campaign's current selection unchanged; pass null to go back to checking every format.`
3912
+ );
3913
+ var IGNORE_FIRST_N_DOMAINS_DESCRIPTION = "How many leading domains of the redirect chain to exclude from detection and tagging (0-5). Use it when the check enters through the caller's OWN click or tracking domains: without it a reputation hit on one of those tags the material and raises an alert about the caller's infrastructure rather than about the offer. Counting starts at the entry point and follows chain order; domains fold to their registrable form, so www.example.com and example.com consume one slot between them, and any other request to one of those domains is excluded too. A skipped domain is checked by NOTHING, so never set this higher than the number of domains the caller actually owns at the head of the chain \u2014 ask rather than guess. The full redirect chain is still captured and returned either way.";
3914
+ var ignoreFirstNDomains = z.number().int().min(0).max(5);
3915
+ var ignoreFirstNDomainsField = ignoreFirstNDomains.optional().describe(`${IGNORE_FIRST_N_DOMAINS_DESCRIPTION} Omit it to skip nothing (0).`);
3916
+ var ignoreFirstNDomainsScanField = ignoreFirstNDomains.optional().describe(
3917
+ `${IGNORE_FIRST_N_DOMAINS_DESCRIPTION} Omit it to skip nothing (0). A scan created directly does NOT inherit this from the campaign named in \`campaign_id\` \u2014 only scans queued by a campaign run do \u2014 so send it explicitly on every direct submission that needs it.`
3918
+ );
3919
+ var ignoreFirstNDomainsUpdateField = ignoreFirstNDomains.optional().describe(
3920
+ `${IGNORE_FIRST_N_DOMAINS_DESCRIPTION} Omitting the field leaves the campaign's current setting unchanged; pass 0 to go back to skipping nothing. Not nullable \u2014 null is rejected with 422.`
3921
+ );
3893
3922
  var MAX_DISCOVERED_ADS_DESCRIPTION = 'How many ad blocks to look for on each publisher page before stopping (1-25). Every ad found becomes its own scan with its own report and is BILLED AS A SEPARATE CHECK, so this multiplies the cost: a page scanned at 25 can cost 26 checks \u2014 one parent plus its children. Only valid on an ad-discovery target \u2014 `ad_discovery: true` on a scan, or `campaign_type: "ad_discovery"` on a campaign \u2014 and rejected with 422 anywhere else.';
3894
3923
  var PLATFORM_DEFAULT_NOTE = "The platform default is 12 unless an operator retuned it.";
3895
3924
  var maxDiscoveredAds = z.number().int().min(1).max(25);
@@ -3993,12 +4022,16 @@ var CreateCampaignInputShape = {
3993
4022
  "VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (required if campaign_type=vast)."
3994
4023
  ),
3995
4024
  referrer: campaignReferrerField,
4025
+ ignore_first_n_domains: ignoreFirstNDomainsField,
3996
4026
  max_discovered_ads: maxDiscoveredAdsField,
4027
+ ad_formats: adFormatsField,
3997
4028
  country_codes: z.array(z.string().length(2)).min(1).describe("ISO 3166-1 alpha-2 codes \u2014 one scan per country per run."),
3998
4029
  group_id: z.string().uuid().optional().describe("Parent group UUID; defaults to the org's default group."),
3999
4030
  ...campaignConfigFields,
4000
4031
  labels: z.record(z.string()).optional().describe("Arbitrary metadata applied to every queued scan."),
4001
- policy_set_id: z.string().uuid().optional().describe("Policy set to evaluate every scan against."),
4032
+ policy_set_id: z.union([z.string().uuid(), z.null()]).optional().describe(
4033
+ "Policy set to evaluate every scan against. Omit to bind the organization's default (if any). Pass null for no policy set and no policy alerts."
4034
+ ),
4002
4035
  schedule_enabled: z.boolean().optional().describe("If true, the scheduler runs immediately. Default: false (manual run).")
4003
4036
  };
4004
4037
  var createCampaignTool = {
@@ -4021,7 +4054,9 @@ var createCampaignTool = {
4021
4054
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
4022
4055
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
4023
4056
  ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
4057
+ ...input.ignore_first_n_domains !== void 0 ? { ignore_first_n_domains: input.ignore_first_n_domains } : {},
4024
4058
  ...input.max_discovered_ads !== void 0 ? { max_discovered_ads: input.max_discovered_ads } : {},
4059
+ ...input.ad_formats !== void 0 ? { ad_formats: input.ad_formats } : {},
4025
4060
  ...input.group_id !== void 0 ? { group_id: input.group_id } : {},
4026
4061
  ...input.labels !== void 0 ? { labels: input.labels } : {},
4027
4062
  ...input.policy_set_id !== void 0 ? { policy_set_id: input.policy_set_id } : {},
@@ -4195,7 +4230,9 @@ var UpdateCampaignInputShape = {
4195
4230
  "New VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (vast-type campaigns)."
4196
4231
  ),
4197
4232
  referrer: campaignReferrerUpdateField,
4233
+ ignore_first_n_domains: ignoreFirstNDomainsUpdateField,
4198
4234
  max_discovered_ads: maxDiscoveredAdsUpdateField,
4235
+ ad_formats: adFormatsUpdateField,
4199
4236
  country_codes: z.array(z.string().length(2)).optional().describe("Replace the country list."),
4200
4237
  group_id: z.string().uuid().optional().describe("Move the campaign to another group."),
4201
4238
  ...campaignConfigFields,
@@ -4205,7 +4242,7 @@ var UpdateCampaignInputShape = {
4205
4242
  };
4206
4243
  var updateCampaignTool = {
4207
4244
  name: "update_campaign",
4208
- description: "Update one or more fields of a campaign. Fields not supplied are left unchanged. `policy_set_id` accepts null to clear the binding, `referrer` accepts null to clear the publisher page scans are checked from, and `max_discovered_ads` accepts null to go back to the platform ad cap.",
4245
+ description: "Update one or more fields of a campaign. Fields not supplied are left unchanged. `policy_set_id` accepts null to clear the binding, `referrer` accepts null to clear the publisher page scans are checked from, `max_discovered_ads` accepts null to go back to the platform ad cap, and `ad_formats` accepts null to go back to checking every ad format. `ignore_first_n_domains` is the exception: it is not nullable, so pass 0 to go back to skipping nothing.",
4209
4246
  annotations: {
4210
4247
  title: "Update Campaign",
4211
4248
  readOnlyHint: false,
@@ -4221,7 +4258,9 @@ var updateCampaignTool = {
4221
4258
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
4222
4259
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
4223
4260
  ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
4261
+ ...input.ignore_first_n_domains !== void 0 ? { ignore_first_n_domains: input.ignore_first_n_domains } : {},
4224
4262
  ...input.max_discovered_ads !== void 0 ? { max_discovered_ads: input.max_discovered_ads } : {},
4263
+ ...input.ad_formats !== void 0 ? { ad_formats: input.ad_formats } : {},
4225
4264
  ...input.country_codes !== void 0 ? { country_codes: input.country_codes } : {},
4226
4265
  ...input.group_id !== void 0 ? { group_id: input.group_id } : {},
4227
4266
  ...input.labels !== void 0 ? { labels: input.labels } : {},
@@ -4235,21 +4274,26 @@ var updateCampaignTool = {
4235
4274
  }
4236
4275
  };
4237
4276
  var PATTERN_RULE_TYPES = ["regexp_request_url", "regexp_request_body"];
4277
+ var PATTERN_RULE_TARGETS = ["page", "creative", "creative_and_page"];
4238
4278
  var patternRuleConfigSchema = z.object({
4239
4279
  pattern: z.string().min(1).max(4096).describe("Non-empty regular expression, at most 4,096 characters."),
4240
4280
  flags: z.enum(["", "i"]).optional().describe("Omit or use '' for case-sensitive matching; use 'i' to ignore case.")
4241
4281
  }).strict();
4242
- var PATTERN_RULE_CONFIG_DOC = "For `rule_type='regexp_request_url'` and `rule_type='regexp_request_body'`, `config` must be exactly `{ pattern: string, flags?: '' | 'i' }`: pattern is non-empty and at most 4,096 characters; omit flags or use `''` for case-sensitive matching, or `'i'` for case-insensitive matching. `target` must be `'page'` for both. `regexp_request_url` inspects up to 5,000 captured request URLs on a fresh scan; tests and rechecks of stored scans reconstruct main-frame hops plus up to 200 persisted subrequests with selected resource types omitted, so historical matching is best-effort. `regexp_request_body` inspects the CONTENTS of the page's scripts, fetch/XHR responses and iframe documents \u2014 never images, video, fonts or stylesheets \u2014 capped at 400 resources, 128 KB each and 8 MB per scan. Those contents are kept for ONE DAY, so a test or recheck against an older scan reports no match because there is nothing left to read.";
4282
+ var PATTERN_RULE_CONFIG_DOC = "For `rule_type='regexp_request_url'` and `rule_type='regexp_request_body'`, `config` must be exactly `{ pattern: string, flags?: '' | 'i' }`: pattern is non-empty and at most 4,096 characters; omit flags or use `''` for case-sensitive matching, or `'i'` for case-insensitive matching. `target` may be `'page'` (the landing tab), `'creative'` (the ad-tag creative's own traffic) or `'creative_and_page'` (both, reported separately) \u2014 a script inside a banner's iframe is on no landing's request list, so a page-only rule cannot see it. Default `page`. `regexp_request_url` inspects up to 5,000 captured request URLs on a fresh scan; tests and rechecks of stored scans reconstruct main-frame hops plus up to 200 persisted subrequests with selected resource types omitted, so historical matching is best-effort. `regexp_request_body` inspects the CONTENTS of the page's scripts, fetch/XHR responses and iframe documents \u2014 never images, video, fonts or stylesheets \u2014 capped at 400 resources, 128 KB each and 8 MB per scan. Those contents are kept for ONE DAY, so a test or recheck against an older scan reports no match because there is nothing left to read.";
4243
4283
  function isPatternRuleType(ruleType) {
4244
4284
  return PATTERN_RULE_TYPES.some((known) => known === ruleType);
4245
4285
  }
4286
+ function isPatternRuleTarget(target) {
4287
+ return PATTERN_RULE_TARGETS.some((known) => known === target);
4288
+ }
4246
4289
  function patternRuleInputError(input) {
4247
4290
  if (!isPatternRuleType(input.rule_type)) return null;
4248
- if (input.target !== void 0 && input.target !== "page") {
4291
+ if (input.target !== void 0 && !isPatternRuleTarget(input.target)) {
4292
+ const allowed = PATTERN_RULE_TARGETS.join(", ");
4249
4293
  return {
4250
4294
  kind: "invalid-input",
4251
- message: `${input.rule_type} requires target='page'.`,
4252
- fieldErrors: { target: [`Must be 'page' for ${input.rule_type}.`] }
4295
+ message: `${input.rule_type} accepts target: ${allowed}.`,
4296
+ fieldErrors: { target: [`Must be one of: ${allowed}.`] }
4253
4297
  };
4254
4298
  }
4255
4299
  const parsed = patternRuleConfigSchema.safeParse(input.config);
@@ -4292,12 +4336,12 @@ var CreateCustomRuleInputShape = {
4292
4336
  "Rule-type-specific configuration object. Shape depends on `rule_type`. " + PATTERN_RULE_CONFIG_DOC + " `regexp_url` remains redirect-chain-only. 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
4293
4337
  ),
4294
4338
  target: z.string().max(30).optional().describe(
4295
- "Where to apply the rule (e.g. 'page' for landing HTML). `regexp_request_url` and `regexp_request_body` require `target='page'`. Default: page. See API docs for the full set of valid values."
4339
+ "Where to apply the rule (e.g. 'page' for landing HTML). `regexp_request_url` and `regexp_request_body` also accept `'creative'` (the ad-tag creative's own traffic) and `'creative_and_page'` (both, reported separately). Default: page. See API docs for the full set of valid values."
4296
4340
  )
4297
4341
  };
4298
4342
  var createCustomRuleTool = {
4299
4343
  name: "create_custom_rule",
4300
- description: "Define a custom tag-detection rule. Use `rule_type='regexp_request_url'` to match captured network and subresource URLs on the fixed `page` target; fresh scans carry up to 5,000 URLs, while later tests/rechecks use a reduced persisted request tree and are best-effort. Use `rule_type='regexp_request_body'` to match the CONTENTS of those sub-resources instead \u2014 the right choice when the code you want to catch keeps changing its filename; those contents are kept for one day. `rule_type='regexp_url'` remains redirect-chain-only. The API auto-registers a tag definition for each emitted slug and rejects built-in system-slug collisions with HTTP 422 / `checking.system_slug_reserved`. Matches tag future scans; existing scans are untouched until `recheck_scans`.",
4344
+ description: "Define a custom tag-detection rule. Use `rule_type='regexp_request_url'` to match captured network and subresource URLs; fresh scans carry up to 5,000 URLs, while later tests/rechecks use a reduced persisted request tree and are best-effort. Use `rule_type='regexp_request_body'` to match the CONTENTS of those sub-resources instead \u2014 the right choice when the code you want to catch keeps changing its filename; those contents are kept for one day. `rule_type='regexp_url'` remains redirect-chain-only. The API auto-registers a tag definition for each emitted slug and rejects built-in system-slug collisions with HTTP 422 / `checking.system_slug_reserved`. Matches tag future scans; existing scans are untouched until `recheck_scans`.",
4301
4345
  annotations: {
4302
4346
  title: "Create Custom Rule",
4303
4347
  readOnlyHint: false,
@@ -4388,7 +4432,7 @@ var TestCustomRuleInputShape = {
4388
4432
  "Rule-type-specific config to test. Same shape as `create_custom_rule`'s `config`. " + PATTERN_RULE_CONFIG_DOC + " `regexp_url` remains redirect-chain-only. 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
4389
4433
  ),
4390
4434
  target: z.string().max(30).describe(
4391
- "Where to apply the rule (e.g. 'page' for landing HTML). `regexp_request_url` and `regexp_request_body` require `target='page'`. See API docs for the full set of valid values."
4435
+ "Where to apply the rule (e.g. 'page' for landing HTML). `regexp_request_url` and `regexp_request_body` also accept `'creative'` and `'creative_and_page'`. See API docs for the full set of valid values."
4392
4436
  ),
4393
4437
  scan_id: z.string().uuid().describe("Existing scan UUID to evaluate the rule against.")
4394
4438
  };
@@ -4428,13 +4472,13 @@ var UpdateCustomRuleInputShape = {
4428
4472
  "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. " + PATTERN_RULE_CONFIG_DOC + " Read the rule first because `rule_type` is immutable and is not repeated in this update input. " + COMBO_MATCH_SCOPE_DOC
4429
4473
  ),
4430
4474
  target: z.string().max(30).optional().describe(
4431
- "Where to apply the rule. `regexp_request_url` and `regexp_request_body` are fixed to `page`; do not change it. See API docs for the valid targets of other rule types."
4475
+ "Where to apply the rule. `regexp_request_url` and `regexp_request_body` accept `'page'`, `'creative'` or `'creative_and_page'`. See API docs for the valid targets of other rule types."
4432
4476
  ),
4433
4477
  is_active: z.boolean().optional().describe("Enable/disable the rule.")
4434
4478
  };
4435
4479
  var updateCustomRuleTool = {
4436
4480
  name: "update_custom_rule",
4437
- description: "Update a custom tag-detection rule. Only supplied fields are sent, but `config` replaces the stored object wholesale; read the rule first and resend every required key. `regexp_request_url` and `regexp_request_body` need a non-empty pattern (max 4,096), flags `''`/`'i'`, and the fixed `page` target. Same-slug GLOBAL rule edits preserve separately managed tag metadata; use `update_tag_definition` to change it. Existing scans are not re-evaluated until `recheck_scans`.",
4481
+ description: "Update a custom tag-detection rule. Only supplied fields are sent, but `config` replaces the stored object wholesale; read the rule first and resend every required key. `regexp_request_url` and `regexp_request_body` need a non-empty pattern (max 4,096), flags `''`/`'i'`, and a target of `'page'`, `'creative'` or `'creative_and_page'`. Same-slug GLOBAL rule edits preserve separately managed tag metadata; use `update_tag_definition` to change it. Existing scans are not re-evaluated until `recheck_scans`.",
4438
4482
  annotations: {
4439
4483
  title: "Update Custom Rule",
4440
4484
  readOnlyHint: false,
@@ -4989,7 +5033,7 @@ var GetPolicySetInputShape = {
4989
5033
  };
4990
5034
  var getPolicySetTool = {
4991
5035
  name: "get_policy_set",
4992
- description: "Get one policy set by UUID with its complete list of entries (each entry is one of five rule kinds \u2014 tag / iab_v3 / brand / ai_category / custom_taxonomy \u2014 plus applicable country codes).",
5036
+ description: "Get one policy set by UUID with its complete list of entries (each entry is one of five rule kinds \u2014 tag / iab_v3 / brand / ai_category / custom_taxonomy \u2014 plus applicable country codes). `is_default` is true when this owned set is the organization's default for new campaigns.",
4993
5037
  annotations: {
4994
5038
  title: "Get Policy Set",
4995
5039
  readOnlyHint: true,
@@ -5040,7 +5084,7 @@ var ListPolicySetsInputShape = {
5040
5084
  };
5041
5085
  var listPolicySetsTool = {
5042
5086
  name: "list_policy_sets",
5043
- description: "Paginated list of policy sets: named collections of violation rules (tag / IAB V3 / brand / AI category / custom-taxonomy entries) that define what counts as a violation. Campaigns bind to one policy set. Returns `{items, total, page, limit}`. List items omit `entries` for payload size \u2014 fetch a single set via `get_policy_set` when you need them. Use `visibility=public` to discover Kaminari Ad-curated sets.",
5087
+ description: "Paginated list of policy sets: named collections of violation rules (tag / IAB V3 / brand / AI category / custom-taxonomy entries) that define what counts as a violation. Campaigns bind to one policy set. Returns `{items, total, page, limit}`. List items omit `entries` for payload size \u2014 fetch a single set via `get_policy_set` when you need them. `is_default` is true on the owned set that new campaigns bind when `policy_set_id` is omitted. Use `visibility=public` to discover Kaminari Ad-curated sets.",
5044
5088
  annotations: {
5045
5089
  title: "List Policy Sets",
5046
5090
  readOnlyHint: true,
@@ -5079,6 +5123,29 @@ var requestPolicySetApprovalTool = {
5079
5123
  return ok({ requested: true });
5080
5124
  }
5081
5125
  };
5126
+ var SetDefaultPolicySetInputShape = {
5127
+ policy_set_id: z.string().uuid().describe("Owned policy set UUID."),
5128
+ is_default: z.boolean().describe(
5129
+ "True makes this the organization's default for new campaigns (clearing any previous default). False clears the flag on this set and leaves the org without a default."
5130
+ )
5131
+ };
5132
+ var setDefaultPolicySetTool = {
5133
+ name: "set_default_policy_set",
5134
+ description: "Mark or clear one of your organization's policy sets as the default for new campaigns. At most one owned set may be default. Creating a campaign without `policy_set_id` then binds this set (or stays unbound if none is set). Pass `policy_set_id: null` on create_campaign to opt out of the default. Foreign public sets cannot be made default. The API returns 204; this tool echoes `is_default`.",
5135
+ annotations: {
5136
+ title: "Set Default Policy Set",
5137
+ readOnlyHint: false,
5138
+ destructiveHint: false,
5139
+ idempotentHint: true,
5140
+ openWorldHint: false
5141
+ },
5142
+ inputSchema: z.object(SetDefaultPolicySetInputShape),
5143
+ handler: async (input, ctx) => {
5144
+ const result = await ctx.api.setDefaultPolicySet(input.policy_set_id, input.is_default);
5145
+ if (result.isErr()) return err(mapApiError(result.error));
5146
+ return ok({ is_default: input.is_default });
5147
+ }
5148
+ };
5082
5149
  var UnpublishPolicySetInputShape = {
5083
5150
  policy_set_id: z.string().uuid().describe("Policy set UUID.")
5084
5151
  };
@@ -5271,6 +5338,7 @@ var CreateBulkScansInputShape = {
5271
5338
  "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`."
5272
5339
  ),
5273
5340
  referrer: scanReferrerField,
5341
+ ignore_first_n_domains: ignoreFirstNDomainsScanField,
5274
5342
  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."),
5275
5343
  emulator_id: z.string().min(1).max(100).describe("Device/OS profile slug; same for every country in the batch."),
5276
5344
  proxy: scanProxyField,
@@ -5296,6 +5364,7 @@ var createBulkScansTool = {
5296
5364
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
5297
5365
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
5298
5366
  ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
5367
+ ...input.ignore_first_n_domains !== void 0 ? { ignore_first_n_domains: input.ignore_first_n_domains } : {},
5299
5368
  ...input.proxy !== void 0 ? { proxy: input.proxy } : {},
5300
5369
  ...input.labels !== void 0 ? { labels: input.labels } : {},
5301
5370
  ...pickRepeatRetryBody(input)
@@ -5316,6 +5385,7 @@ var CreateScanInputShape = {
5316
5385
  "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`."
5317
5386
  ),
5318
5387
  referrer: scanReferrerField,
5388
+ ignore_first_n_domains: ignoreFirstNDomainsScanField,
5319
5389
  country_code: z.string().length(2).describe("ISO 3166-1 alpha-2 country code, e.g. US, DE, JP. Determines proxy geo."),
5320
5390
  emulator_id: z.string().min(1).max(100).describe("Device/OS profile slug; use `list_emulators` to discover valid values."),
5321
5391
  proxy: scanProxyField,
@@ -5323,9 +5393,10 @@ var CreateScanInputShape = {
5323
5393
  campaign_id: z.string().uuid().optional().describe("Optional campaign UUID to attribute the scan to."),
5324
5394
  run_id: z.string().uuid().optional().describe("Optional run UUID inside the campaign."),
5325
5395
  ad_discovery: z.boolean().optional().describe(
5326
- "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`."
5396
+ "Publisher ad discovery: detect ad blocks on the page and spawn one child scan per detected ad (banner/video/pop). Only valid with `url`. Each child is a separate billed scan; list them with `list_scan_children`."
5327
5397
  ),
5328
5398
  max_discovered_ads: maxDiscoveredAdsField,
5399
+ ad_formats: adFormatsField,
5329
5400
  ...repeatRetryFields
5330
5401
  };
5331
5402
  var createScanTool = {
@@ -5347,12 +5418,14 @@ var createScanTool = {
5347
5418
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
5348
5419
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
5349
5420
  ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
5421
+ ...input.ignore_first_n_domains !== void 0 ? { ignore_first_n_domains: input.ignore_first_n_domains } : {},
5350
5422
  ...input.proxy !== void 0 ? { proxy: input.proxy } : {},
5351
5423
  ...input.labels !== void 0 ? { labels: input.labels } : {},
5352
5424
  ...input.campaign_id !== void 0 ? { campaign_id: input.campaign_id } : {},
5353
5425
  ...input.run_id !== void 0 ? { run_id: input.run_id } : {},
5354
5426
  ...input.ad_discovery !== void 0 ? { ad_discovery: input.ad_discovery } : {},
5355
5427
  ...input.max_discovered_ads !== void 0 ? { max_discovered_ads: input.max_discovered_ads } : {},
5428
+ ...input.ad_formats !== void 0 ? { ad_formats: input.ad_formats } : {},
5356
5429
  ...pickRepeatRetryBody(input)
5357
5430
  };
5358
5431
  const result = await ctx.api.createScan(body);
@@ -6131,6 +6204,7 @@ function registerAllTools(register) {
6131
6204
  register(deletePolicySetTool);
6132
6205
  register(requestPolicySetApprovalTool);
6133
6206
  register(unpublishPolicySetTool);
6207
+ register(setDefaultPolicySetTool);
6134
6208
  register(listPolicySetCampaignsTool);
6135
6209
  register(attachPolicySetCampaignsTool);
6136
6210
  register(detachPolicySetCampaignsTool);
@@ -6240,5 +6314,5 @@ function formatToolError(error) {
6240
6314
  }
6241
6315
 
6242
6316
  export { BearerToken, SERVER_INSTRUCTIONS, createHttpApiGateway, createPinoLogger, declareEmptyResourcesAndPrompts, newRequestId, wireToolsIntoMcpServer };
6243
- //# sourceMappingURL=chunk-NPSDASQE.js.map
6244
- //# sourceMappingURL=chunk-NPSDASQE.js.map
6317
+ //# sourceMappingURL=chunk-XJZE6KTY.js.map
6318
+ //# sourceMappingURL=chunk-XJZE6KTY.js.map