@kaminari-ad/mcp 0.14.1 → 0.17.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,124 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.17.0] - 2026-09-03
11
+
12
+ ### Added
13
+
14
+ - **`regexp_request_body` — rules can match what a page's sub-resources
15
+ contain.** The three custom-rule writers (`create_custom_rule`,
16
+ `update_custom_rule`, `test_custom_rule`) now accept the new rule type
17
+ and enforce its contract locally: the same `{pattern, flags}` config as
18
+ `regexp_request_url` and the same fixed `page` target, rejected before
19
+ the request leaves the process.
20
+
21
+ It exists because address-based matching stopped being enough. The
22
+ malvertising kits rotate every filename on every visit, so a
23
+ `regexp_request_url` pattern matches the names it was written against
24
+ and nothing after; the code inside those files changes far more slowly.
25
+
26
+ Two limits the tool descriptions now state, because both look like a
27
+ non-match when they bite: only the scripts, fetch/XHR responses and
28
+ iframe documents are captured (never images, video, fonts or
29
+ stylesheets), up to 400 resources, 128 KB each and 8 MB per scan — and
30
+ the captured contents are kept for **one day**, so
31
+ `test_custom_rule` against an older scan reports no match with nothing
32
+ left to read.
33
+
34
+ Needs the api and crawler sides deployed first.
35
+
36
+ ### Changed
37
+
38
+ - The internal request-URL rule guard is now a shared pattern-rule guard
39
+ covering both types, so their contracts cannot drift apart.
40
+
41
+ ## [0.16.0] - 2026-08-26
42
+
43
+ ### Added
44
+
45
+ - **`unpublish_policy_set` — publication is no longer a one-way door
46
+ (KAMIAD-167).** `request_policy_set_approval` had no inverse, so an
47
+ agent could put a set into the shared catalog but never take it out;
48
+ the only escape was asking a human to reject it, and that worked only
49
+ while the request was still pending. The new tool returns a set to
50
+ private from either state — approved or awaiting review — and is
51
+ idempotent on a set that is already private. It is annotated
52
+ `destructiveHint: true` because other organizations lose access to a
53
+ set they may be browsing. Campaigns already bound to the set keep
54
+ running against it; going private blocks new attachments instead.
55
+
56
+ Needs the API side deployed first: it wraps
57
+ `POST /api/v1/policy-sets/{id}/unpublish`, added in `adverif/api!378`.
58
+
59
+ ### Changed
60
+
61
+ - **`get_campaign_alert_overrides` validates `mode` strictly — this
62
+ actually shipped in 0.15.0, undocumented.** That release's regen
63
+ narrowed `CampaignOverridesResponse.mode` from a bare `string` to the
64
+ generated `CampaignOverrideMode` enum (`inherit | override |
65
+ silence`), and the response parser enforces it, so a fourth value
66
+ from the API now surfaces as an `upstream` error instead of passing
67
+ through. That is the intent — the api types the field, so drift
68
+ should fail loudly — but it is a behaviour change and 0.15.0 recorded
69
+ only "regenerated openapi.ts / zod-schemas.ts".
70
+ - `set_campaign_alert_overrides` now takes its `mode` input from that
71
+ same generated enum instead of a hand-written `z.enum` with identical
72
+ values. Behaviour is unchanged; the point is that `check-tool-enum-drift`
73
+ can finally catch future value changes on this field, so its
74
+ exemption is gone.
75
+
76
+ ### Removed
77
+
78
+ - The two `/api/forms/*` entries in `check-api-coverage`'s
79
+ `EXEMPT_OPERATIONS`. Those routes are `include_in_schema=False` and
80
+ dropped out of the spec in the 0.15.0 regen, so the exemptions had
81
+ become dead config — exactly as their own comment predicted.
82
+
83
+ ## [0.15.0] - 2026-08-24
84
+
85
+ ### Added
86
+
87
+ - **`get_proxy_targeting` — the accepted proxy values, instead of
88
+ guesswork.** A scan's `proxy` block takes a region, city, and ISP whose
89
+ valid values come from our upstream network provider's catalogue, so
90
+ they cannot be an enum in the schema: they differ per country and per
91
+ connection type. Until now an agent had to guess, and a wrong guess
92
+ came back as a 422 with a prose message. The new tool wraps
93
+ `GET /api/v1/proxy/targeting` and returns the accepted regions,
94
+ cities, and ISPs for a country, ordered by pool size so the agent can
95
+ prefer values likely to yield an exit node. Anything it lists is
96
+ accepted by `create_scan`.
97
+
98
+ Two things the tool's description makes explicit, because both are
99
+ easy to get wrong: pass `proxy_type: "mobile"` when the scan is mobile
100
+ (the two networks are separate pools — in the US that is 1500+ ISPs
101
+ against roughly a dozen carriers), and take a city from the same
102
+ response that produced the region, since a region and a city that do
103
+ not belong together leave the provider nothing to route through.
104
+
105
+ - **The write tools now point at it.** A tool nobody is told about is a
106
+ tool nobody calls, and an agent starts from `create_scan`, not from the
107
+ catalogue. The `proxy` fields on `create_scan` / `create_bulk_scans`
108
+ and the `proxy_*` fields on `create_campaign` / `update_campaign` now
109
+ name `get_proxy_targeting` and carry the same two warnings. The
110
+ campaign `proxy_region` description previously said "free-text", which
111
+ told the agent to invent a value — precisely the behaviour that
112
+ produces the 422 this release exists to remove.
113
+
114
+ ### Changed
115
+
116
+ - **Regenerated `openapi.ts` / `zod-schemas.ts`** against the API
117
+ release that adds `/api/v1/proxy/targeting`.
118
+
119
+ - **A rejected field value now arrives as a `detail` array.** The API
120
+ changed 422 responses for values checked against a runtime vocabulary
121
+ (`proxy.region`, `proxy.city`, `proxy.isp`, `country_code`,
122
+ `emulator_id`) from a prose string to the `HTTPValidationError`
123
+ array its OpenAPI schema always declared. No change was needed here —
124
+ `toApiError` has handled both shapes since the parser-drift work — and
125
+ the agent-visible message is now `body.proxy.region: Unsupported proxy
126
+ region …` instead of an undifferentiated sentence.
127
+
10
128
  ## [0.14.1] - 2026-08-24
11
129
 
12
130
  ### Changed
@@ -1145,7 +1263,8 @@ Initial public release. The first version that ships to npm under
1145
1263
  need them.
1146
1264
  - Invoice PDF fetcher — same reason.
1147
1265
 
1148
- [Unreleased]: https://github.com/kaminari-ad/mcp/compare/v0.14.1...HEAD
1266
+ [Unreleased]: https://github.com/kaminari-ad/mcp/compare/v0.15.0...HEAD
1267
+ [0.15.0]: https://github.com/kaminari-ad/mcp/compare/v0.14.1...v0.15.0
1149
1268
  [0.14.1]: https://github.com/kaminari-ad/mcp/compare/v0.14.0...v0.14.1
1150
1269
  [0.14.0]: https://github.com/kaminari-ad/mcp/compare/v0.13.0...v0.14.0
1151
1270
  [0.13.0]: https://github.com/kaminari-ad/mcp/compare/v0.12.0...v0.13.0
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 105 tools exposed.
66
+ Restart your client. You should see `kaminari-ad` in the MCP servers list with 107 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
- 105 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
+ 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:
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,13 +123,13 @@ 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** (9) — `list_policy_sets`, `get_policy_set`, `create_policy_set`, `update_policy_set`, `delete_policy_set`, `request_policy_set_approval`, `list_policy_set_campaigns`, `attach_policy_set_campaigns`, `detach_policy_set_campaigns`
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`
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`
130
130
  - **Invoicing** (2) — `list_invoices`, `get_invoice_pdf`
131
131
  - **Alert notifications** (5) — `list_alert_destinations`, `delete_alert_destination`, `set_alert_destination_version`, `get_campaign_alert_overrides`, `set_campaign_alert_overrides`
132
- - **Reference data** (2) — `list_geos`, `list_emulators`
132
+ - **Reference data** (3) — `list_geos`, `list_emulators`, `get_proxy_targeting`
133
133
 
134
134
  Screenshots (`get_scan_screenshot`, `get_scan_creative_screenshot`, `get_scan_landing_screenshot`) come back as inline MCP `image` blocks; `get_invoice_pdf` and `get_scan_creative_video` as inline resource blocks — no second fetch, no presigned URL. The two text artifacts (`get_scan_creative_html`, `get_scan_vast_xml`) come back as strings the model can read directly. Every artifact download is size-capped in the gateway — 256 KiB for the text artifacts, 8 MiB for the binary ones — and refused while reading rather than buffered and then rejected.
135
135
 
package/dist/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { NAME, VERSION, err, ok } from './chunk-YS7IDW24.js';
2
+ import { NAME, VERSION, err, ok } from './chunk-OX65SNEN.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-II6IVJWD.js');
163
+ const { bootstrapStdio } = await import('./stdio-bootstrap-NVDBVJ7K.js');
164
164
  return bootstrapStdio(config);
165
165
  }
166
- const { bootstrapHttp } = await import('./http-bootstrap-XAA75RA3.js');
166
+ const { bootstrapHttp } = await import('./http-bootstrap-DKGGQURI.js');
167
167
  return bootstrapHttp(config);
168
168
  }
169
169
  main().then(
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { err, ok } from './chunk-YS7IDW24.js';
2
+ import { err, ok } from './chunk-OX65SNEN.js';
3
3
  import { createHash, randomUUID } from 'crypto';
4
4
  import createClient from 'openapi-fetch';
5
5
  import { fetch } from 'undici';
@@ -471,6 +471,15 @@ var GeoResponse = z.object({
471
471
  region: z.string(),
472
472
  tier: z.string()
473
473
  }).passthrough();
474
+ var ProxyTargetingResponse = z.object({
475
+ country_code: z.string(),
476
+ proxy_type: z.string(),
477
+ regions: z.array(z.string()),
478
+ cities: z.array(z.string()),
479
+ isps: z.array(z.string()),
480
+ refreshed_at: z.union([z.string(), z.null()]),
481
+ ttl_seconds: z.number().int()
482
+ }).passthrough();
474
483
  var EmulatorResponse = z.object({
475
484
  id: z.string(),
476
485
  display_name: z.string(),
@@ -1093,13 +1102,14 @@ var AlertNotificationDestinationResponse = z.object({
1093
1102
  updated_at: z.string().datetime({ offset: true })
1094
1103
  }).passthrough();
1095
1104
  z.object({ version: AlertNotificationVersion }).passthrough();
1105
+ var CampaignOverrideMode = z.enum(["inherit", "override", "silence"]);
1096
1106
  var CampaignOverridesResponse = z.object({
1097
1107
  campaign_id: z.string().uuid(),
1098
- mode: z.string(),
1108
+ mode: CampaignOverrideMode,
1099
1109
  destination_ids: z.array(z.string().uuid())
1100
1110
  }).passthrough();
1101
1111
  z.object({
1102
- mode: z.string().min(1).max(16),
1112
+ mode: CampaignOverrideMode,
1103
1113
  destination_ids: z.array(z.string().uuid()).optional().default([])
1104
1114
  }).passthrough();
1105
1115
  var InvoiceType = z.enum(["proforma", "final"]);
@@ -1135,32 +1145,6 @@ z.object({
1135
1145
  limit: z.number().int(),
1136
1146
  pages: z.number().int()
1137
1147
  }).passthrough();
1138
- z.object({
1139
- name: z.string().min(2).max(100),
1140
- email: z.string().email(),
1141
- message: z.string().min(10).max(2e3),
1142
- source: z.string().max(512).optional().default("")
1143
- }).passthrough();
1144
- z.object({
1145
- id: z.string().uuid(),
1146
- received_at: z.string().datetime({ offset: true })
1147
- }).passthrough();
1148
- var PreferredContactChannel = z.enum(["telegram", "whatsapp", "email"]);
1149
- z.object({
1150
- first_name: z.string().min(2).max(60),
1151
- last_name: z.string().min(2).max(60),
1152
- company_email: z.string().email(),
1153
- company_name: z.string().min(2).max(120),
1154
- preferred_channel: PreferredContactChannel,
1155
- contact_handle: z.string().max(120).optional().default(""),
1156
- comment: z.string().max(2e3).optional().default(""),
1157
- privacy_accepted: z.boolean(),
1158
- source: z.string().max(512).optional().default("")
1159
- }).passthrough();
1160
- z.object({
1161
- id: z.string().uuid(),
1162
- received_at: z.string().datetime({ offset: true })
1163
- }).passthrough();
1164
1148
  var CustomTaxonomyListItem = z.object({
1165
1149
  id: z.string().uuid(),
1166
1150
  name: z.string(),
@@ -1230,6 +1214,7 @@ var schemas = {
1230
1214
  ScanResponse,
1231
1215
  ScanBriefResponse,
1232
1216
  GeoResponse,
1217
+ ProxyTargetingResponse,
1233
1218
  EmulatorResponse,
1234
1219
  CampaignGroupResponse,
1235
1220
  GroupActionResponse,
@@ -1261,6 +1246,7 @@ var schemas = {
1261
1246
  DeliveryAttemptResponse,
1262
1247
  BulkReplayResponse,
1263
1248
  AlertNotificationDestinationResponse,
1249
+ CampaignOverrideMode,
1264
1250
  CampaignOverridesResponse,
1265
1251
  InvoiceResponse,
1266
1252
  CustomTaxonomyListItem,
@@ -1779,6 +1765,18 @@ var PolicySetListItemSchema = schemas.PolicySetListItem.pick({
1779
1765
  }).strip();
1780
1766
  var parsePolicySetPage = (raw) => parsePagedWithItemSchema(PolicySetListItemSchema, raw, "policy-sets");
1781
1767
 
1768
+ // src/infrastructure/api/parsers/parse-proxy-targeting.ts
1769
+ var ProxyTargetingSchema = schemas.ProxyTargetingResponse.pick({
1770
+ country_code: true,
1771
+ proxy_type: true,
1772
+ regions: true,
1773
+ cities: true,
1774
+ isps: true,
1775
+ refreshed_at: true,
1776
+ ttl_seconds: true
1777
+ }).strip();
1778
+ var parseProxyTargeting = (raw) => parseWithSchema(ProxyTargetingSchema, raw, "proxy targeting");
1779
+
1782
1780
  // src/infrastructure/api/parsers/parse-run.ts
1783
1781
  var RunSchema = schemas.RunResponse.pick({
1784
1782
  id: true,
@@ -2254,6 +2252,9 @@ function createHttpApiGateway(config) {
2254
2252
  async listEmulators() {
2255
2253
  return call("GET", "/api/v1/emulators", {}, parseEmulatorList);
2256
2254
  },
2255
+ async getProxyTargeting(query) {
2256
+ return call("GET", "/api/v1/proxy/targeting", { params: { query } }, parseProxyTargeting);
2257
+ },
2257
2258
  // ── Campaigns ─────────────────────────────────────────────────
2258
2259
  async listCampaigns(filters) {
2259
2260
  return call("GET", "/api/v1/campaigns", { params: { query: filters } }, parseCampaignPage);
@@ -2556,6 +2557,14 @@ function createHttpApiGateway(config) {
2556
2557
  parseEmpty
2557
2558
  );
2558
2559
  },
2560
+ async unpublishPolicySet(id) {
2561
+ return call(
2562
+ "POST",
2563
+ "/api/v1/policy-sets/{policy_set_id}/unpublish",
2564
+ { params: { path: { policy_set_id: id } } },
2565
+ parseEmpty
2566
+ );
2567
+ },
2559
2568
  // ── Custom taxonomies ─────────────────────────────────────────
2560
2569
  async listCustomTaxonomies(filters) {
2561
2570
  return call(
@@ -3288,12 +3297,7 @@ var setAlertDestinationVersionTool = {
3288
3297
  };
3289
3298
  var SetCampaignAlertOverridesInputShape = {
3290
3299
  campaign_id: z.string().uuid().describe("Campaign UUID."),
3291
- // enum-drift: allow — the API validates these three values but types
3292
- // the field as a plain string, so no generated schema constrains it.
3293
- // Swap to `schemas.CampaignOverrideMode` after the api enum change
3294
- // (`fix/type-override-mode-and-unlist-forms`) reaches prod and the
3295
- // next `make gen-api-types` picks it up.
3296
- mode: z.enum(["inherit", "override", "silence"]).describe(
3300
+ mode: schemas.CampaignOverrideMode.describe(
3297
3301
  "Routing mode: `inherit` (fall back to the org-wide destinations), `override` (route ONLY to `destination_ids`), `silence` (send nothing for this campaign)."
3298
3302
  ),
3299
3303
  destination_ids: z.array(z.string().uuid()).max(50).default([]).describe(
@@ -3920,10 +3924,18 @@ var campaignConfigFields = {
3920
3924
  emulator_mode: z.enum(["random", "all"]).optional().describe(
3921
3925
  "How selected categories expand per run: 'random' = one random device per category (the UI's 'Random (1 per group)'), 'all' = every device in each category ('All checked'). Does not affect emulator_specific_ids. Default: random."
3922
3926
  ),
3923
- proxy_type: z.enum(["residential", "mobile"]).optional().describe("Proxy network type. Default: residential."),
3924
- proxy_region: z.string().optional().describe("Proxy region/state targeting (free-text; only honoured for a single country)."),
3925
- proxy_city: z.string().optional().describe("Proxy city targeting."),
3926
- proxy_isp: z.string().optional().describe("Proxy ISP targeting."),
3927
+ proxy_type: z.enum(["residential", "mobile"]).optional().describe(
3928
+ "Proxy network type. Default: residential. Residential and mobile are separate pools with separate catalogues, so pass the same value to `get_proxy_targeting` that you send here."
3929
+ ),
3930
+ proxy_region: z.string().optional().describe(
3931
+ "Proxy region/state; use a value from `get_proxy_targeting` for one of this campaign's countries. Only honoured when the campaign targets a single country."
3932
+ ),
3933
+ proxy_city: z.string().optional().describe(
3934
+ "Proxy city from `get_proxy_targeting`. If you also set `proxy_region`, take the city from a call made with that same region \u2014 a city from a different region passes validation but leaves the provider no exit node."
3935
+ ),
3936
+ proxy_isp: z.string().optional().describe(
3937
+ "Proxy ISP, or mobile carrier when `proxy_type` is mobile. Use a value from `get_proxy_targeting`."
3938
+ ),
3927
3939
  ...repeatRetryFields,
3928
3940
  schedule_type: z.enum(["weekly", "interval"]).optional().describe(
3929
3941
  "Scheduling mode: 'weekly' (run on a weekday/hour grid via schedule_weekly) or 'interval' (run every schedule_interval_seconds). Omit for a manual (run-on-demand) campaign."
@@ -4211,28 +4223,32 @@ var updateCampaignTool = {
4211
4223
  return ok(result.value);
4212
4224
  }
4213
4225
  };
4214
- var requestUrlRuleConfigSchema = z.object({
4215
- pattern: z.string().min(1).max(4096).describe("Non-empty request-URL regular expression, at most 4,096 characters."),
4226
+ var PATTERN_RULE_TYPES = ["regexp_request_url", "regexp_request_body"];
4227
+ var patternRuleConfigSchema = z.object({
4228
+ pattern: z.string().min(1).max(4096).describe("Non-empty regular expression, at most 4,096 characters."),
4216
4229
  flags: z.enum(["", "i"]).optional().describe("Omit or use '' for case-sensitive matching; use 'i' to ignore case.")
4217
4230
  }).strict();
4218
- var REQUEST_URL_RULE_CONFIG_DOC = "For `rule_type='regexp_request_url'`, `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'`. Fresh scans inspect up to 5,000 captured request URLs. 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.";
4219
- function requestUrlRuleInputError(input) {
4220
- if (input.rule_type !== "regexp_request_url") return null;
4231
+ 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.";
4232
+ function isPatternRuleType(ruleType) {
4233
+ return PATTERN_RULE_TYPES.some((known) => known === ruleType);
4234
+ }
4235
+ function patternRuleInputError(input) {
4236
+ if (!isPatternRuleType(input.rule_type)) return null;
4221
4237
  if (input.target !== void 0 && input.target !== "page") {
4222
4238
  return {
4223
4239
  kind: "invalid-input",
4224
- message: "regexp_request_url requires target='page'.",
4225
- fieldErrors: { target: ["Must be 'page' for regexp_request_url."] }
4240
+ message: `${input.rule_type} requires target='page'.`,
4241
+ fieldErrors: { target: [`Must be 'page' for ${input.rule_type}.`] }
4226
4242
  };
4227
4243
  }
4228
- const parsed = requestUrlRuleConfigSchema.safeParse(input.config);
4244
+ const parsed = patternRuleConfigSchema.safeParse(input.config);
4229
4245
  if (parsed.success) return null;
4230
4246
  const issue = parsed.error.issues[0];
4231
4247
  const field = issue?.path.length ? `config.${issue.path.join(".")}` : "config";
4232
- const message = issue?.message ?? "Invalid request-URL rule config.";
4248
+ const message = issue?.message ?? "Invalid pattern rule config.";
4233
4249
  return {
4234
4250
  kind: "invalid-input",
4235
- message: `Invalid regexp_request_url config: ${message}`,
4251
+ message: `Invalid ${input.rule_type} config: ${message}`,
4236
4252
  fieldErrors: { [field]: [message] }
4237
4253
  };
4238
4254
  }
@@ -4248,10 +4264,7 @@ var ruleConfigField = z.record(z.unknown()).superRefine((config, ctx) => {
4248
4264
  });
4249
4265
  }
4250
4266
  });
4251
- var requestUrlAwareRuleConfigField = z.union([
4252
- requestUrlRuleConfigSchema,
4253
- ruleConfigField
4254
- ]);
4267
+ var patternAwareRuleConfigField = z.union([patternRuleConfigSchema, ruleConfigField]);
4255
4268
 
4256
4269
  // src/application/tools/custom-rules/create-custom-rule.tool.ts
4257
4270
  var CreateCustomRuleInputShape = {
@@ -4262,18 +4275,18 @@ var CreateCustomRuleInputShape = {
4262
4275
  "Tag slug to assign on match. Empty = create-only (advanced). The API auto-registers a custom tag definition for this slug with `display_name = name`. **MUST NOT collide with a built-in system tag slug** (see `list_tags` where `scope=system`); colliding requests return 422 with code `checking.system_slug_reserved`. For `rule_type='llm'` use `config.tags` keys instead and leave `tag_slug` empty."
4263
4276
  ),
4264
4277
  rule_type: z.string().max(50).describe(
4265
- "Rule engine. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `regexp_request_url`, `blacklist_domain`, `combo`, `llm`. `regexp_url` checks redirect-chain URLs only; `regexp_request_url` checks captured network and subresource URLs. The API validates."
4278
+ "Rule engine. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `regexp_request_url`, `regexp_request_body`, `blacklist_domain`, `combo`, `llm`. `regexp_url` checks redirect-chain URLs only; `regexp_request_url` checks captured network and subresource URLs; `regexp_request_body` checks what those sub-resources contained. The API validates."
4266
4279
  ),
4267
- config: requestUrlAwareRuleConfigField.describe(
4268
- "Rule-type-specific configuration object. Shape depends on `rule_type`. " + REQUEST_URL_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
4280
+ config: patternAwareRuleConfigField.describe(
4281
+ "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
4269
4282
  ),
4270
4283
  target: z.string().max(30).optional().describe(
4271
- "Where to apply the rule (e.g. 'page' for landing HTML). `regexp_request_url` requires `target='page'`. Default: page. See API docs for the full set of valid values."
4284
+ "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."
4272
4285
  )
4273
4286
  };
4274
4287
  var createCustomRuleTool = {
4275
4288
  name: "create_custom_rule",
4276
- 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. `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`.",
4289
+ 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`.",
4277
4290
  annotations: {
4278
4291
  title: "Create Custom Rule",
4279
4292
  readOnlyHint: false,
@@ -4283,7 +4296,7 @@ var createCustomRuleTool = {
4283
4296
  },
4284
4297
  inputSchema: z.object(CreateCustomRuleInputShape),
4285
4298
  handler: async (input, ctx) => {
4286
- const inputError = requestUrlRuleInputError(input);
4299
+ const inputError = patternRuleInputError(input);
4287
4300
  if (inputError) return err(inputError);
4288
4301
  const body = {
4289
4302
  name: input.name,
@@ -4358,19 +4371,19 @@ var listCustomRulesTool = {
4358
4371
  };
4359
4372
  var TestCustomRuleInputShape = {
4360
4373
  rule_type: z.string().max(50).describe(
4361
- "Rule engine type. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `regexp_request_url`, `blacklist_domain`, `combo`, `llm`. `regexp_url` checks redirect-chain URLs only; `regexp_request_url` checks captured network and subresource URLs."
4374
+ "Rule engine type. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `regexp_request_url`, `regexp_request_body`, `blacklist_domain`, `combo`, `llm`. `regexp_url` checks redirect-chain URLs only; `regexp_request_url` checks captured network and subresource URLs; `regexp_request_body` checks what those sub-resources contained."
4362
4375
  ),
4363
- config: requestUrlAwareRuleConfigField.describe(
4364
- "Rule-type-specific config to test. Same shape as `create_custom_rule`'s `config`. " + REQUEST_URL_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
4376
+ config: patternAwareRuleConfigField.describe(
4377
+ "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
4365
4378
  ),
4366
4379
  target: z.string().max(30).describe(
4367
- "Where to apply the rule (e.g. 'page' for landing HTML). `regexp_request_url` requires `target='page'`. See API docs for the full set of valid values."
4380
+ "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."
4368
4381
  ),
4369
4382
  scan_id: z.string().uuid().describe("Existing scan UUID to evaluate the rule against.")
4370
4383
  };
4371
4384
  var testCustomRuleTool = {
4372
4385
  name: "test_custom_rule",
4373
- description: "Preview-test a rule definition against a stored scan without persisting it. For `regexp_request_url`, the historical snapshot contains main-frame hops plus at most 200 persisted subrequests with selected resource types omitted, so a no-match does not prove the original fresh scan lacked the request. Returns match state, elapsed time, and per-tag detail; the preview response does not expose the matched request URL separately. Slug-collision validation does not run in preview mode.",
4386
+ description: "Preview-test a rule definition against a stored scan without persisting it. For `regexp_request_url`, the historical snapshot contains main-frame hops plus at most 200 persisted subrequests with selected resource types omitted, so a no-match does not prove the original fresh scan lacked the request. For `regexp_request_body`, the captured contents live for ONE DAY \u2014 pick a scan from the last 24 hours, because an older one reports no match with nothing left to read. Returns match state, elapsed time, and per-tag detail; the preview response does not expose the matched request URL separately. Slug-collision validation does not run in preview mode.",
4374
4387
  annotations: {
4375
4388
  title: "Test Custom Rule",
4376
4389
  readOnlyHint: true,
@@ -4380,7 +4393,7 @@ var testCustomRuleTool = {
4380
4393
  },
4381
4394
  inputSchema: z.object(TestCustomRuleInputShape),
4382
4395
  handler: async (input, ctx) => {
4383
- const inputError = requestUrlRuleInputError(input);
4396
+ const inputError = patternRuleInputError(input);
4384
4397
  if (inputError) return err(inputError);
4385
4398
  const result = await ctx.api.testCustomRule({
4386
4399
  rule_type: input.rule_type,
@@ -4400,17 +4413,17 @@ var UpdateCustomRuleInputShape = {
4400
4413
  tag_slug: z.string().max(100).optional().describe(
4401
4414
  "New tag slug to assign on match. **MUST NOT collide with a built-in system tag slug** (see `list_tags` where `scope=system`); colliding requests return 422 with code `checking.system_slug_reserved`. Leaving a GLOBAL rule on the same slug preserves its admin-managed tag metadata."
4402
4415
  ),
4403
- config: requestUrlAwareRuleConfigField.optional().describe(
4404
- "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. " + REQUEST_URL_RULE_CONFIG_DOC + " Read the rule first because `rule_type` is immutable and is not repeated in this update input. " + COMBO_MATCH_SCOPE_DOC
4416
+ config: patternAwareRuleConfigField.optional().describe(
4417
+ "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
4405
4418
  ),
4406
4419
  target: z.string().max(30).optional().describe(
4407
- "Where to apply the rule. `regexp_request_url` is fixed to `page`; do not change it. See API docs for the valid targets of other rule types."
4420
+ "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."
4408
4421
  ),
4409
4422
  is_active: z.boolean().optional().describe("Enable/disable the rule.")
4410
4423
  };
4411
4424
  var updateCustomRuleTool = {
4412
4425
  name: "update_custom_rule",
4413
- 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` needs 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`.",
4426
+ 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`.",
4414
4427
  annotations: {
4415
4428
  title: "Update Custom Rule",
4416
4429
  readOnlyHint: false,
@@ -5055,6 +5068,26 @@ var requestPolicySetApprovalTool = {
5055
5068
  return ok({ requested: true });
5056
5069
  }
5057
5070
  };
5071
+ var UnpublishPolicySetInputShape = {
5072
+ policy_set_id: z.string().uuid().describe("Policy set UUID.")
5073
+ };
5074
+ var unpublishPolicySetTool = {
5075
+ name: "unpublish_policy_set",
5076
+ description: "Return one of your organization's policy sets to PRIVATE. Inverse of request_policy_set_approval: removes an approved set from the shared catalog other organizations browse, or withdraws a publication request that is still awaiting review. The set stays fully usable inside your own organization. Campaigns in other organizations that already reference the set keep their binding \u2014 going private blocks new attachments rather than breaking existing ones. Safe to call on a set that is already private.",
5077
+ annotations: {
5078
+ title: "Unpublish Policy Set",
5079
+ readOnlyHint: false,
5080
+ destructiveHint: true,
5081
+ idempotentHint: true,
5082
+ openWorldHint: false
5083
+ },
5084
+ inputSchema: z.object(UnpublishPolicySetInputShape),
5085
+ handler: async (input, ctx) => {
5086
+ const result = await ctx.api.unpublishPolicySet(input.policy_set_id);
5087
+ if (result.isErr()) return err(mapApiError(result.error));
5088
+ return ok({ unpublished: true });
5089
+ }
5090
+ };
5058
5091
  var UpdatePolicySetInputShape = {
5059
5092
  policy_set_id: z.string().uuid().describe("Policy set UUID."),
5060
5093
  name: z.string().min(1).max(200).describe("New name (always required by the API on update)."),
@@ -5084,6 +5117,38 @@ var updatePolicySetTool = {
5084
5117
  return ok(result.value);
5085
5118
  }
5086
5119
  };
5120
+ var GetProxyTargetingInputShape = {
5121
+ country_code: z.string().length(2).describe("ISO 3166-1 alpha-2 country code, e.g. 'US'. Case-insensitive."),
5122
+ proxy_type: z.enum(["residential", "mobile"]).optional().describe(
5123
+ "Which network to describe. Defaults to residential. Pass 'mobile' when the scan will use mobile: the two are separate pools with different catalogues, so a residential ISP is normally rejected for a mobile scan."
5124
+ ),
5125
+ region: z.string().optional().describe(
5126
+ "Narrow the returned cities to one region. Pass a value from this tool's own 'regions' array. Omit it for the country-wide city list."
5127
+ )
5128
+ };
5129
+ var getProxyTargetingTool = {
5130
+ name: "get_proxy_targeting",
5131
+ description: "List the proxy regions, cities, and ISPs accepted for a country. Call this before setting proxy targeting on create_scan, create_bulk_scans, create_campaign, or update_campaign \u2014 the accepted values come from the upstream network provider and cannot be listed in those tools' schemas, so a guess is a 422. Anything listed here is accepted. Values are ordered by pool size, largest first; prefer values near the front. Two traps: residential and mobile are separate pools, so pass the proxy_type the scan will use; and without a region the cities are the whole country's, so if you target a region, call again with that region and take the city from that narrower list \u2014 a region and a city that do not belong together are accepted but leave the provider no exit node.",
5132
+ annotations: {
5133
+ title: "Get Proxy Targeting",
5134
+ readOnlyHint: true,
5135
+ destructiveHint: false,
5136
+ idempotentHint: true,
5137
+ openWorldHint: false
5138
+ },
5139
+ inputSchema: z.object(GetProxyTargetingInputShape),
5140
+ handler: async (input, ctx) => {
5141
+ const result = await ctx.api.getProxyTargeting({
5142
+ country_code: input.country_code,
5143
+ ...input.proxy_type !== void 0 && { proxy_type: input.proxy_type },
5144
+ ...input.region !== void 0 && { region: input.region }
5145
+ });
5146
+ if (result.isErr()) {
5147
+ return err(mapApiError(result.error));
5148
+ }
5149
+ return ok(result.value);
5150
+ }
5151
+ };
5087
5152
  var CancelRunInputShape = { run_id: z.string().uuid().describe("Run UUID.") };
5088
5153
  var cancelRunTool = {
5089
5154
  name: "cancel_run",
@@ -5166,11 +5231,21 @@ var cancelScanTool = {
5166
5231
  }
5167
5232
  };
5168
5233
  var scanProxyField = z.object({
5169
- proxy_type: z.enum(["residential", "mobile"]).optional().describe("Proxy network type. Default: residential."),
5170
- region: z.string().optional().describe("Proxy region/state targeting."),
5171
- city: z.string().optional().describe("Proxy city targeting."),
5172
- isp: z.string().optional().describe("Proxy ISP targeting.")
5173
- }).optional().describe("Optional proxy geo targeting for the crawl. Omit to use the org default.");
5234
+ proxy_type: z.enum(["residential", "mobile"]).optional().describe(
5235
+ "Proxy network type. Default: residential. Residential and mobile are separate pools with separate catalogues, so pass the same value to `get_proxy_targeting` that you send here."
5236
+ ),
5237
+ region: z.string().optional().describe(
5238
+ "Proxy region/state. Use a value from `get_proxy_targeting`; anything else is rejected with 422."
5239
+ ),
5240
+ city: z.string().optional().describe(
5241
+ "Proxy city. Use a value from `get_proxy_targeting`. If you also set `region`, take the city from a `get_proxy_targeting` call made with that same region \u2014 a city from a different region passes validation but leaves the provider no exit node."
5242
+ ),
5243
+ isp: z.string().optional().describe(
5244
+ "Proxy ISP, or mobile carrier when `proxy_type` is mobile. Use a value from `get_proxy_targeting`."
5245
+ )
5246
+ }).optional().describe(
5247
+ "Optional proxy geo targeting for the crawl. Omit to use the org default. Call `get_proxy_targeting` for the country first: the accepted values come from the upstream network provider and cannot be listed in this schema."
5248
+ );
5174
5249
  var scanReferrerField = z.string().url().regex(/^https?:\/\//i, "Must be an http(s) URL.").max(2048).optional().describe(
5175
5250
  "Optional http(s) page URL the check is performed from. With `ad_tag` or `vast_tag` this is the publisher page the tag is embedded in \u2014 NOT the creative and NOT its landing page; the browser commits the page document on this URL without fetching the publisher, so the creative renders as if embedded there, and its cross-origin subrequests receive the origin only (https://publisher.example/, no path). With a `url` target this is where the visitor came from, sent in full as the Referer of the page request."
5176
5251
  );
@@ -5980,6 +6055,7 @@ function registerAllTools(register) {
5980
6055
  register(createApiKeyTool);
5981
6056
  register(revokeApiKeyTool);
5982
6057
  register(listGeosTool);
6058
+ register(getProxyTargetingTool);
5983
6059
  register(listEmulatorsTool);
5984
6060
  register(getScanTool);
5985
6061
  register(listScanChildrenTool);
@@ -6041,6 +6117,7 @@ function registerAllTools(register) {
6041
6117
  register(updatePolicySetTool);
6042
6118
  register(deletePolicySetTool);
6043
6119
  register(requestPolicySetApprovalTool);
6120
+ register(unpublishPolicySetTool);
6044
6121
  register(listPolicySetCampaignsTool);
6045
6122
  register(attachPolicySetCampaignsTool);
6046
6123
  register(detachPolicySetCampaignsTool);
@@ -6150,5 +6227,5 @@ function formatToolError(error) {
6150
6227
  }
6151
6228
 
6152
6229
  export { BearerToken, SERVER_INSTRUCTIONS, createHttpApiGateway, createPinoLogger, declareEmptyResourcesAndPrompts, newRequestId, wireToolsIntoMcpServer };
6153
- //# sourceMappingURL=chunk-IEOPWWSC.js.map
6154
- //# sourceMappingURL=chunk-IEOPWWSC.js.map
6230
+ //# sourceMappingURL=chunk-FA5A6Y4H.js.map
6231
+ //# sourceMappingURL=chunk-FA5A6Y4H.js.map