@kaminari-ad/mcp 0.19.1 → 0.22.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,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.22.0] - 2026-09-15
11
+
12
+ ### Changed
13
+
14
+ - **`regexp_request_url` and `regexp_request_body` accept the creative
15
+ surface.** Both were rejected locally with anything but `target='page'`,
16
+ which made the rule that catches an exploit kit embedded in a banner
17
+ unauthorable: that kit is a script inside the creative's own iframe and
18
+ appears on no landing page's request list. Both now take `'page'`,
19
+ `'creative'` or `'creative_and_page'` (evaluated on each surface, every
20
+ match reporting the one it came from), matching what the API validates.
21
+ Tool descriptions updated with it, since an agent picks the target from
22
+ them.
23
+
24
+ Requires the API-side deploy that widens the same validator. Until it
25
+ lands, production rejects the two new targets with HTTP 422.
26
+
27
+ ## [0.21.0] - 2026-09-14
28
+
29
+ > Requires the API-side KAMIAD-177 deploy. Until it lands, production
30
+ > rejects `ignore_first_n_domains` as an unknown body field, so do not tag
31
+ > this release ahead of the API.
32
+
33
+ ### Added
34
+
35
+ - **`ignore_first_n_domains` on `create_campaign`, `update_campaign`,
36
+ `create_scan` and `create_bulk_scans`.** A check that enters through the
37
+ caller's own click or tracking domains used to have those domains rated
38
+ by every domain checker, so a reputation hit on the caller's own
39
+ infrastructure tagged the material and raised an alert that said nothing
40
+ about the offer. The new field (0-5, default 0) drops the first N unique
41
+ domains of the redirect chain, counted from the entry point, from
42
+ detection and tagging. Domains fold to their registrable form, so
43
+ `www.example.com` and `example.com` consume one slot between them, and
44
+ sub-requests made from a skipped domain are excluded too.
45
+
46
+ Deliberately NOT nullable, unlike `referrer` and `max_discovered_ads`
47
+ next to it: `0` is both the default and the reset, so a nullable variant
48
+ would only offer an agent a second spelling the API answers with a 422.
49
+ The shared `.describe()` text warns that a skipped domain is checked by
50
+ nothing, because the failure mode of guessing high is a hidden finding
51
+ rather than an error.
52
+
53
+ `CampaignResponse` and `ScanResponse` both echo the field, so an agent can
54
+ read back what a campaign will do and how much of a finished scan's chain
55
+ was excluded — the latter is the only way to answer that once the campaign
56
+ behind the scan has been edited. Parsed as optional rather than defaulted
57
+ to 0: an api that predates the field says nothing, and inventing a 0 would
58
+ claim the scan skipped nothing when the truth is unknown.
59
+
60
+ The generated schemas were left alone: the field is declared through the
61
+ same port-side intersection `max_discovered_ads` uses, because
62
+ `gen:api-types` reads the DEPLOYED spec and cannot see an unshipped
63
+ field. The next regen absorbs it and the intersections can go.
64
+
65
+ ## [0.20.0] - 2026-09-13
66
+
67
+ ### Added
68
+
69
+ - **`set_default_policy_set`** — mark or clear an owned policy set as
70
+ the organization's default for new campaigns (`POST /api/v1/policy-sets/{id}/set-default`).
71
+ `list_policy_sets` / `get_policy_set` now surface `is_default`. Omit
72
+ `policy_set_id` on `create_campaign` to bind that default; pass
73
+ `null` to create an unbound campaign.
74
+
10
75
  ## [0.19.1] - 2026-09-10
11
76
 
12
77
  > 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-J6SALJR4.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-6PXVYXHT.js');
164
164
  return bootstrapStdio(config);
165
165
  }
166
- const { bootstrapHttp } = await import('./http-bootstrap-FN4DOOJT.js');
166
+ const { bootstrapHttp } = await import('./http-bootstrap-JX5RQALF.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.22.0";
7
7
 
8
8
  export { NAME, VERSION };
9
- //# sourceMappingURL=chunk-MKBTS4IH.js.map
10
- //# sourceMappingURL=chunk-MKBTS4IH.js.map
9
+ //# sourceMappingURL=chunk-J6SALJR4.js.map
10
+ //# sourceMappingURL=chunk-J6SALJR4.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-J6SALJR4.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.22.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-J6SALJR4.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({ 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({ 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,15 @@ var cancelCampaignTool = {
3890
3903
  return ok(result.value);
3891
3904
  }
3892
3905
  };
3906
+ 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.";
3907
+ var ignoreFirstNDomains = z.number().int().min(0).max(5);
3908
+ var ignoreFirstNDomainsField = ignoreFirstNDomains.optional().describe(`${IGNORE_FIRST_N_DOMAINS_DESCRIPTION} Omit it to skip nothing (0).`);
3909
+ var ignoreFirstNDomainsScanField = ignoreFirstNDomains.optional().describe(
3910
+ `${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.`
3911
+ );
3912
+ var ignoreFirstNDomainsUpdateField = ignoreFirstNDomains.optional().describe(
3913
+ `${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.`
3914
+ );
3893
3915
  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
3916
  var PLATFORM_DEFAULT_NOTE = "The platform default is 12 unless an operator retuned it.";
3895
3917
  var maxDiscoveredAds = z.number().int().min(1).max(25);
@@ -3993,12 +4015,15 @@ var CreateCampaignInputShape = {
3993
4015
  "VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (required if campaign_type=vast)."
3994
4016
  ),
3995
4017
  referrer: campaignReferrerField,
4018
+ ignore_first_n_domains: ignoreFirstNDomainsField,
3996
4019
  max_discovered_ads: maxDiscoveredAdsField,
3997
4020
  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
4021
  group_id: z.string().uuid().optional().describe("Parent group UUID; defaults to the org's default group."),
3999
4022
  ...campaignConfigFields,
4000
4023
  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."),
4024
+ policy_set_id: z.union([z.string().uuid(), z.null()]).optional().describe(
4025
+ "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."
4026
+ ),
4002
4027
  schedule_enabled: z.boolean().optional().describe("If true, the scheduler runs immediately. Default: false (manual run).")
4003
4028
  };
4004
4029
  var createCampaignTool = {
@@ -4021,6 +4046,7 @@ var createCampaignTool = {
4021
4046
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
4022
4047
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
4023
4048
  ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
4049
+ ...input.ignore_first_n_domains !== void 0 ? { ignore_first_n_domains: input.ignore_first_n_domains } : {},
4024
4050
  ...input.max_discovered_ads !== void 0 ? { max_discovered_ads: input.max_discovered_ads } : {},
4025
4051
  ...input.group_id !== void 0 ? { group_id: input.group_id } : {},
4026
4052
  ...input.labels !== void 0 ? { labels: input.labels } : {},
@@ -4195,6 +4221,7 @@ var UpdateCampaignInputShape = {
4195
4221
  "New VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (vast-type campaigns)."
4196
4222
  ),
4197
4223
  referrer: campaignReferrerUpdateField,
4224
+ ignore_first_n_domains: ignoreFirstNDomainsUpdateField,
4198
4225
  max_discovered_ads: maxDiscoveredAdsUpdateField,
4199
4226
  country_codes: z.array(z.string().length(2)).optional().describe("Replace the country list."),
4200
4227
  group_id: z.string().uuid().optional().describe("Move the campaign to another group."),
@@ -4205,7 +4232,7 @@ var UpdateCampaignInputShape = {
4205
4232
  };
4206
4233
  var updateCampaignTool = {
4207
4234
  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.",
4235
+ 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. `ignore_first_n_domains` is the exception: it is not nullable, so pass 0 to go back to skipping nothing.",
4209
4236
  annotations: {
4210
4237
  title: "Update Campaign",
4211
4238
  readOnlyHint: false,
@@ -4221,6 +4248,7 @@ var updateCampaignTool = {
4221
4248
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
4222
4249
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
4223
4250
  ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
4251
+ ...input.ignore_first_n_domains !== void 0 ? { ignore_first_n_domains: input.ignore_first_n_domains } : {},
4224
4252
  ...input.max_discovered_ads !== void 0 ? { max_discovered_ads: input.max_discovered_ads } : {},
4225
4253
  ...input.country_codes !== void 0 ? { country_codes: input.country_codes } : {},
4226
4254
  ...input.group_id !== void 0 ? { group_id: input.group_id } : {},
@@ -4235,21 +4263,26 @@ var updateCampaignTool = {
4235
4263
  }
4236
4264
  };
4237
4265
  var PATTERN_RULE_TYPES = ["regexp_request_url", "regexp_request_body"];
4266
+ var PATTERN_RULE_TARGETS = ["page", "creative", "creative_and_page"];
4238
4267
  var patternRuleConfigSchema = z.object({
4239
4268
  pattern: z.string().min(1).max(4096).describe("Non-empty regular expression, at most 4,096 characters."),
4240
4269
  flags: z.enum(["", "i"]).optional().describe("Omit or use '' for case-sensitive matching; use 'i' to ignore case.")
4241
4270
  }).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.";
4271
+ 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
4272
  function isPatternRuleType(ruleType) {
4244
4273
  return PATTERN_RULE_TYPES.some((known) => known === ruleType);
4245
4274
  }
4275
+ function isPatternRuleTarget(target) {
4276
+ return PATTERN_RULE_TARGETS.some((known) => known === target);
4277
+ }
4246
4278
  function patternRuleInputError(input) {
4247
4279
  if (!isPatternRuleType(input.rule_type)) return null;
4248
- if (input.target !== void 0 && input.target !== "page") {
4280
+ if (input.target !== void 0 && !isPatternRuleTarget(input.target)) {
4281
+ const allowed = PATTERN_RULE_TARGETS.join(", ");
4249
4282
  return {
4250
4283
  kind: "invalid-input",
4251
- message: `${input.rule_type} requires target='page'.`,
4252
- fieldErrors: { target: [`Must be 'page' for ${input.rule_type}.`] }
4284
+ message: `${input.rule_type} accepts target: ${allowed}.`,
4285
+ fieldErrors: { target: [`Must be one of: ${allowed}.`] }
4253
4286
  };
4254
4287
  }
4255
4288
  const parsed = patternRuleConfigSchema.safeParse(input.config);
@@ -4292,12 +4325,12 @@ var CreateCustomRuleInputShape = {
4292
4325
  "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
4326
  ),
4294
4327
  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."
4328
+ "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
4329
  )
4297
4330
  };
4298
4331
  var createCustomRuleTool = {
4299
4332
  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`.",
4333
+ 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
4334
  annotations: {
4302
4335
  title: "Create Custom Rule",
4303
4336
  readOnlyHint: false,
@@ -4388,7 +4421,7 @@ var TestCustomRuleInputShape = {
4388
4421
  "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
4422
  ),
4390
4423
  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."
4424
+ "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
4425
  ),
4393
4426
  scan_id: z.string().uuid().describe("Existing scan UUID to evaluate the rule against.")
4394
4427
  };
@@ -4428,13 +4461,13 @@ var UpdateCustomRuleInputShape = {
4428
4461
  "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
4462
  ),
4430
4463
  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."
4464
+ "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
4465
  ),
4433
4466
  is_active: z.boolean().optional().describe("Enable/disable the rule.")
4434
4467
  };
4435
4468
  var updateCustomRuleTool = {
4436
4469
  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`.",
4470
+ 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
4471
  annotations: {
4439
4472
  title: "Update Custom Rule",
4440
4473
  readOnlyHint: false,
@@ -4989,7 +5022,7 @@ var GetPolicySetInputShape = {
4989
5022
  };
4990
5023
  var getPolicySetTool = {
4991
5024
  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).",
5025
+ 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
5026
  annotations: {
4994
5027
  title: "Get Policy Set",
4995
5028
  readOnlyHint: true,
@@ -5040,7 +5073,7 @@ var ListPolicySetsInputShape = {
5040
5073
  };
5041
5074
  var listPolicySetsTool = {
5042
5075
  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.",
5076
+ 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
5077
  annotations: {
5045
5078
  title: "List Policy Sets",
5046
5079
  readOnlyHint: true,
@@ -5079,6 +5112,29 @@ var requestPolicySetApprovalTool = {
5079
5112
  return ok({ requested: true });
5080
5113
  }
5081
5114
  };
5115
+ var SetDefaultPolicySetInputShape = {
5116
+ policy_set_id: z.string().uuid().describe("Owned policy set UUID."),
5117
+ is_default: z.boolean().describe(
5118
+ "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."
5119
+ )
5120
+ };
5121
+ var setDefaultPolicySetTool = {
5122
+ name: "set_default_policy_set",
5123
+ 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`.",
5124
+ annotations: {
5125
+ title: "Set Default Policy Set",
5126
+ readOnlyHint: false,
5127
+ destructiveHint: false,
5128
+ idempotentHint: true,
5129
+ openWorldHint: false
5130
+ },
5131
+ inputSchema: z.object(SetDefaultPolicySetInputShape),
5132
+ handler: async (input, ctx) => {
5133
+ const result = await ctx.api.setDefaultPolicySet(input.policy_set_id, input.is_default);
5134
+ if (result.isErr()) return err(mapApiError(result.error));
5135
+ return ok({ is_default: input.is_default });
5136
+ }
5137
+ };
5082
5138
  var UnpublishPolicySetInputShape = {
5083
5139
  policy_set_id: z.string().uuid().describe("Policy set UUID.")
5084
5140
  };
@@ -5271,6 +5327,7 @@ var CreateBulkScansInputShape = {
5271
5327
  "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
5328
  ),
5273
5329
  referrer: scanReferrerField,
5330
+ ignore_first_n_domains: ignoreFirstNDomainsScanField,
5274
5331
  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
5332
  emulator_id: z.string().min(1).max(100).describe("Device/OS profile slug; same for every country in the batch."),
5276
5333
  proxy: scanProxyField,
@@ -5296,6 +5353,7 @@ var createBulkScansTool = {
5296
5353
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
5297
5354
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
5298
5355
  ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
5356
+ ...input.ignore_first_n_domains !== void 0 ? { ignore_first_n_domains: input.ignore_first_n_domains } : {},
5299
5357
  ...input.proxy !== void 0 ? { proxy: input.proxy } : {},
5300
5358
  ...input.labels !== void 0 ? { labels: input.labels } : {},
5301
5359
  ...pickRepeatRetryBody(input)
@@ -5316,6 +5374,7 @@ var CreateScanInputShape = {
5316
5374
  "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
5375
  ),
5318
5376
  referrer: scanReferrerField,
5377
+ ignore_first_n_domains: ignoreFirstNDomainsScanField,
5319
5378
  country_code: z.string().length(2).describe("ISO 3166-1 alpha-2 country code, e.g. US, DE, JP. Determines proxy geo."),
5320
5379
  emulator_id: z.string().min(1).max(100).describe("Device/OS profile slug; use `list_emulators` to discover valid values."),
5321
5380
  proxy: scanProxyField,
@@ -5347,6 +5406,7 @@ var createScanTool = {
5347
5406
  ...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
5348
5407
  ...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
5349
5408
  ...input.referrer !== void 0 ? { referrer: input.referrer } : {},
5409
+ ...input.ignore_first_n_domains !== void 0 ? { ignore_first_n_domains: input.ignore_first_n_domains } : {},
5350
5410
  ...input.proxy !== void 0 ? { proxy: input.proxy } : {},
5351
5411
  ...input.labels !== void 0 ? { labels: input.labels } : {},
5352
5412
  ...input.campaign_id !== void 0 ? { campaign_id: input.campaign_id } : {},
@@ -6131,6 +6191,7 @@ function registerAllTools(register) {
6131
6191
  register(deletePolicySetTool);
6132
6192
  register(requestPolicySetApprovalTool);
6133
6193
  register(unpublishPolicySetTool);
6194
+ register(setDefaultPolicySetTool);
6134
6195
  register(listPolicySetCampaignsTool);
6135
6196
  register(attachPolicySetCampaignsTool);
6136
6197
  register(detachPolicySetCampaignsTool);
@@ -6240,5 +6301,5 @@ function formatToolError(error) {
6240
6301
  }
6241
6302
 
6242
6303
  export { BearerToken, SERVER_INSTRUCTIONS, createHttpApiGateway, createPinoLogger, declareEmptyResourcesAndPrompts, newRequestId, wireToolsIntoMcpServer };
6243
- //# sourceMappingURL=chunk-NPSDASQE.js.map
6244
- //# sourceMappingURL=chunk-NPSDASQE.js.map
6304
+ //# sourceMappingURL=chunk-VEPJEI3S.js.map
6305
+ //# sourceMappingURL=chunk-VEPJEI3S.js.map