@kaminari-ad/mcp 0.18.0 → 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,142 @@ 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
+
75
+ ## [0.19.1] - 2026-09-10
76
+
77
+ > Requires the API-side KAMIAD-104 deploy. Until it lands, production
78
+ > rejects `invoice_payment_adjustment` as an unknown query enum (422,
79
+ > surfaced as an `invalid_input` tool error), so do not tag this release
80
+ > ahead of the API.
81
+
82
+ ### Fixed
83
+
84
+ - **`list_balance_history` can filter on `invoice_payment_adjustment`.**
85
+ The API gained this `BalanceTransactionType` upstream (KAMIAD-104):
86
+ correcting an already-settled invoice appends a signed correction row
87
+ rather than editing the original `invoice_settlement`. Once the API
88
+ ships, an unfiltered call will return such rows without a parser change
89
+ — the response schema holds `type` as a plain string — but the filter
90
+ enum rejected the value, so an agent could neither select nor exclude
91
+ corrections. Same class of gap as `card_top_up` in 0.13.0, hence a
92
+ patch. No tool signature changed.
93
+
94
+ The two generated schemas were hand-edited rather than regenerated,
95
+ because `gen:api-types` defaults to the production spec and would drag
96
+ in unrelated drift that is not on this branch. Each hunk was verified
97
+ byte-equivalent to what a regen against the branch spec produces.
98
+
99
+ ### Security
100
+
101
+ - Cleared the three advisory clusters that `audit:deps` flagged, taking
102
+ `npm audit` from 9 findings (5 high, 4 moderate) to zero. `vitest` and
103
+ its `@vitest/*` siblings 4.1.6 → 4.1.11 (GHSA-82fw-gwwq-j7x9) and the
104
+ `js-yaml` override 4.3.1 → 4.3.2 (GHSA-2883-xcg3-v3hh) are both
105
+ dev-only. The new `hono` override (`^4.13.7`, three advisories) is
106
+ **not**: `hono` reaches the runtime tree through
107
+ `@modelcontextprotocol/sdk`, on the streamable-HTTP transport's request
108
+ path, so this changes what the HTTP deployment resolves. The published
109
+ bundle is unaffected — the SDK stays external — and npm `overrides` do
110
+ not propagate to consumers, so a consumer pinning a vulnerable `hono`
111
+ needs its own bump.
112
+
113
+ ## [0.19.0] - 2026-09-08
114
+
115
+ ### Changed
116
+
117
+ - **`network` on a discovered ad is now the ad network's NAME, and its
118
+ domain moved to `network_host`.** The API used to put whichever ad host
119
+ matched first into `network`, so an agent reading it got
120
+ `cdn.tsyndicate.com` where a name belongs — and sometimes
121
+ `www.googletagmanager.com`, which is not an ad network at all. Upstream
122
+ (KAMIAD-133) `network` became a display name resolved from a domain
123
+ catalog and is **empty when the ad server is not recognised**, while the
124
+ observed domain travels in the new `network_host`.
125
+
126
+ Both fields are exposed on every tool that returns a scan shape:
127
+ `get_scan`, `list_scans`, `list_scan_children`, `create_scan` and
128
+ `create_bulk_scans`. (`list_run_scans` returns the slim run tile, which
129
+ carries no network fields at all.) An
130
+ agent that identified networks by `network` should read `network_host`
131
+ when `network` is empty; one that treated `network` as a hostname needs
132
+ to stop. Scans created before the upstream release keep a hostname in
133
+ `network` and have an empty `network_host`, so a network breakdown is
134
+ only sound over scans created after it.
135
+
136
+ Ads served from infrastructure (tag managers, analytics, measurement,
137
+ DMPs) and from a publisher's own creative CDN are still found, scanned
138
+ and billed exactly as before — they simply no longer claim a network
139
+ name.
140
+
141
+ `network_host` is declared on the port rather than picked from the
142
+ generated types, the same escape hatch `max_discovered_ads` uses: the
143
+ generated files are regenerated off the DEPLOYED spec, and this field
144
+ ships in the release it describes. The next regen absorbs it.
145
+
10
146
  ## [0.17.1] - 2026-09-03
11
147
 
12
148
  ### Fixed
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-KCQ55TRV.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-ZWMO4KXK.js');
163
+ const { bootstrapStdio } = await import('./stdio-bootstrap-6PXVYXHT.js');
164
164
  return bootstrapStdio(config);
165
165
  }
166
- const { bootstrapHttp } = await import('./http-bootstrap-WTWLW2LP.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.18.0";
6
+ var VERSION = "0.22.0";
7
7
 
8
8
  export { NAME, VERSION };
9
- //# sourceMappingURL=chunk-KCQ55TRV.js.map
10
- //# sourceMappingURL=chunk-KCQ55TRV.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-KCQ55TRV.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.18.0\";\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-KCQ55TRV.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({
@@ -978,6 +981,7 @@ var BalanceTransactionType = z.enum([
978
981
  "admin_adjustment",
979
982
  "refund",
980
983
  "invoice_settlement",
984
+ "invoice_payment_adjustment",
981
985
  "crypto_top_up",
982
986
  "card_top_up"
983
987
  ]);
@@ -1376,7 +1380,7 @@ var CampaignSchema = schemas.CampaignResponse.pick({
1376
1380
  is_archived: true,
1377
1381
  created_at: true,
1378
1382
  last_run_at: true
1379
- }).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();
1380
1384
  var parseCampaign = (raw) => parseWithSchema(CampaignSchema, raw, "campaign");
1381
1385
  var parseCampaignPage = (raw) => parsePagedWithItemSchema(CampaignSchema, raw, "campaigns");
1382
1386
 
@@ -1746,6 +1750,7 @@ var PolicySetSchema = schemas.PolicySetResponse.pick({
1746
1750
  organization_id: true,
1747
1751
  visibility: true,
1748
1752
  is_approved: true,
1753
+ is_default: true,
1749
1754
  entries: true,
1750
1755
  created_at: true
1751
1756
  }).strip();
@@ -1759,6 +1764,7 @@ var PolicySetListItemSchema = schemas.PolicySetListItem.pick({
1759
1764
  organization_id: true,
1760
1765
  visibility: true,
1761
1766
  is_approved: true,
1767
+ is_default: true,
1762
1768
  created_at: true
1763
1769
  }).strip();
1764
1770
  var parsePolicySetPage = (raw) => parsePagedWithItemSchema(PolicySetListItemSchema, raw, "policy-sets");
@@ -1838,12 +1844,10 @@ var ScanSchema = schemas.ScanResponse.pick({
1838
1844
  repeat_scan_ids: true,
1839
1845
  retry_attempt: true,
1840
1846
  retry_max_attempts: true
1841
- }).extend({ creative_kind: z.string().default("banner") }).extend({ max_discovered_ads: z.number().int().nullable().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();
1842
1848
  var ScanArraySchema = z.array(ScanSchema);
1843
1849
  var parseScan = (raw) => parseWithSchema(ScanSchema, raw, "scan");
1844
1850
  var parseScanArray = (raw) => parseWithSchema(ScanArraySchema, raw, "scans");
1845
-
1846
- // src/infrastructure/api/parsers/parse-scan-page.ts
1847
1851
  var ScanBriefSchema = schemas.ScanBriefResponse.pick({
1848
1852
  id: true,
1849
1853
  url: true,
@@ -1870,7 +1874,7 @@ var ScanBriefSchema = schemas.ScanBriefResponse.pick({
1870
1874
  repeat_session_id: true,
1871
1875
  retry_attempt: true,
1872
1876
  retry_max_attempts: true
1873
- }).strip();
1877
+ }).extend({ network_host: z.string().optional() }).strip();
1874
1878
  var parseScanPage = (raw) => parsePagedWithItemSchema(ScanBriefSchema, raw, "scans");
1875
1879
  var TagDefinitionSchema = schemas.TagDefinitionWithStatsResponse.pick({
1876
1880
  slug: true,
@@ -2563,6 +2567,14 @@ function createHttpApiGateway(config) {
2563
2567
  parseEmpty
2564
2568
  );
2565
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
+ },
2566
2578
  // ── Custom taxonomies ─────────────────────────────────────────
2567
2579
  async listCustomTaxonomies(filters) {
2568
2580
  return call(
@@ -3550,6 +3562,7 @@ var TransactionTypeEnum = z.enum([
3550
3562
  "admin_adjustment",
3551
3563
  "refund",
3552
3564
  "invoice_settlement",
3565
+ "invoice_payment_adjustment",
3553
3566
  "crypto_top_up",
3554
3567
  "card_top_up"
3555
3568
  ]);
@@ -3557,14 +3570,14 @@ var ListBalanceHistoryInputShape = {
3557
3570
  date_from: z.string().date().optional().describe("ISO date, inclusive."),
3558
3571
  date_to: z.string().date().optional().describe("ISO date, inclusive."),
3559
3572
  type: z.array(TransactionTypeEnum).max(TransactionTypeEnum.options.length).optional().describe(
3560
- "Filter by transaction kind. Pass several values to OR them (e.g. ['top_up_manual','crypto_top_up','card_top_up'] for credits-only)."
3573
+ "Filter by transaction kind. Pass several values to OR them (e.g. ['top_up_manual','crypto_top_up','card_top_up','invoice_settlement'] for every way a balance is funded). Note `invoice_payment_adjustment` is signed: a correction to an already-settled invoice can be either a credit or a debit."
3561
3574
  ),
3562
3575
  page: z.number().int().min(1).max(500).default(1).describe("1-indexed page."),
3563
3576
  limit: z.number().int().min(1).max(200).default(50).describe("Page size.")
3564
3577
  };
3565
3578
  var listBalanceHistoryTool = {
3566
3579
  name: "list_balance_history",
3567
- description: "List ledger transactions (charges, refunds, top-ups, invoice settlements) on the organization's balance. Each row: type, amount in micros, description, timestamp. Filter by `type` (multi-select) and / or date range.",
3580
+ description: "List ledger transactions (charges, refunds, top-ups, invoice settlements and their corrections) on the organization's balance. Each row: type, amount in micros, description, timestamp. Filter by `type` (multi-select) and / or date range.",
3568
3581
  annotations: {
3569
3582
  title: "List Balance History",
3570
3583
  readOnlyHint: true,
@@ -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 } : {},
@@ -5532,7 +5592,7 @@ var ListScanChildrenInputShape = {
5532
5592
  };
5533
5593
  var listScanChildrenTool = {
5534
5594
  name: "list_scan_children",
5535
- description: "List the discovered-ad child scans of a publisher ad-discovery scan \u2014 one per detected ad block on the page. Each child brief carries ad_kind (banner|pop), the attributed ad network, and slot_index. The repeat / retry fields are present but never meaningful here: a child is always its own group of one (`repeat_index` 0, `repeat_total` 1, `repeat_session_id` null) and is never re-crawled, because repeats and retries apply to the publisher scan \u2014 repeating an ad-discovery scan gives you several parents, each with its own children. Returns a paginated envelope with screenshot + report deep-links; link users with those, never hand-build URLs.",
5595
+ description: "List the discovered-ad child scans of a publisher ad-discovery scan \u2014 one per detected ad block on the page. Each child brief carries ad_kind (banner|pop), slot_index, and the ad's attribution split in two: network is the ad network's NAME and is empty when the ad server is unrecognised, network_host is the domain observed \u2014 so read network first and fall back to network_host. Both are empty when the ad came from infrastructure that is not an ad network (tag manager, analytics) or from the publisher's own creative CDN; a pop carries a network but no host, because pop networks rotate their serving domains. The repeat / retry fields are present but never meaningful here: a child is always its own group of one (`repeat_index` 0, `repeat_total` 1, `repeat_session_id` null) and is never re-crawled, because repeats and retries apply to the publisher scan \u2014 repeating an ad-discovery scan gives you several parents, each with its own children. Returns a paginated envelope with screenshot + report deep-links; link users with those, never hand-build URLs.",
5536
5596
  annotations: {
5537
5597
  title: "List Scan Children",
5538
5598
  readOnlyHint: true,
@@ -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-MYBWPFWG.js.map
6244
- //# sourceMappingURL=chunk-MYBWPFWG.js.map
6304
+ //# sourceMappingURL=chunk-VEPJEI3S.js.map
6305
+ //# sourceMappingURL=chunk-VEPJEI3S.js.map