@kaminari-ad/mcp 0.12.1 → 0.14.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 +105 -1
- package/README.md +8 -8
- package/dist/bin.js +3 -3
- package/dist/{chunk-XCIH5HFZ.js → chunk-CUJWJSNJ.js} +552 -61
- package/dist/chunk-CUJWJSNJ.js.map +1 -0
- package/dist/{chunk-MIYKHCDU.js → chunk-FQTVDC5D.js} +3 -3
- package/dist/{chunk-MIYKHCDU.js.map → chunk-FQTVDC5D.js.map} +1 -1
- package/dist/{http-bootstrap-ORWIA6A3.js → http-bootstrap-AYQ47NTT.js} +4 -4
- package/dist/{http-bootstrap-ORWIA6A3.js.map → http-bootstrap-AYQ47NTT.js.map} +1 -1
- package/dist/{stdio-bootstrap-AY2N4EPO.js → stdio-bootstrap-XZIU2X6Z.js} +4 -4
- package/dist/{stdio-bootstrap-AY2N4EPO.js.map → stdio-bootstrap-XZIU2X6Z.js.map} +1 -1
- package/package.json +3 -1
- package/dist/chunk-XCIH5HFZ.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { err, ok } from './chunk-
|
|
2
|
+
import { err, ok } from './chunk-FQTVDC5D.js';
|
|
3
3
|
import { createHash, randomUUID } from 'crypto';
|
|
4
4
|
import createClient from 'openapi-fetch';
|
|
5
5
|
import { fetch } from 'undici';
|
|
@@ -103,6 +103,20 @@ function newRequestId() {
|
|
|
103
103
|
return randomUUID();
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// src/shared/artifact-limits.ts
|
|
107
|
+
var MAX_TEXT_ARTIFACT_BYTES = 256 * 1024;
|
|
108
|
+
var MAX_BINARY_ARTIFACT_BYTES = 8 * 1024 * 1024;
|
|
109
|
+
function formatBytes(bytes) {
|
|
110
|
+
const kib = 1024;
|
|
111
|
+
if (bytes < kib) return `${String(bytes)} B`;
|
|
112
|
+
const mib = kib * kib;
|
|
113
|
+
if (bytes < mib) return `${(bytes / kib).toFixed(1)} KiB`;
|
|
114
|
+
return `${(bytes / mib).toFixed(1)} MiB`;
|
|
115
|
+
}
|
|
116
|
+
function decodeUtf8(bytes) {
|
|
117
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
// src/infrastructure/api/error-mapping.ts
|
|
107
121
|
function detail(parsed) {
|
|
108
122
|
if (parsed === null || typeof parsed !== "object") return "Upstream error";
|
|
@@ -893,7 +907,7 @@ z.object({
|
|
|
893
907
|
filter_date_to: z.union([z.string(), z.null()]).optional(),
|
|
894
908
|
filter_timezone: z.union([z.string(), z.null()]).optional()
|
|
895
909
|
}).passthrough();
|
|
896
|
-
z.object({ updated: z.number().int(), skipped: z.number().int() }).passthrough();
|
|
910
|
+
var BulkUpdateAlertStatusResponse = z.object({ updated: z.number().int(), skipped: z.number().int() }).passthrough();
|
|
897
911
|
var AlertStatsResponse = z.object({
|
|
898
912
|
open: z.number().int(),
|
|
899
913
|
escalated: z.number().int(),
|
|
@@ -1231,9 +1245,11 @@ var schemas = {
|
|
|
1231
1245
|
CustomRuleResponse,
|
|
1232
1246
|
RuleTestResponse,
|
|
1233
1247
|
PolicyEntryResponse,
|
|
1248
|
+
LinkedCampaignResponse,
|
|
1234
1249
|
PolicySetResponse,
|
|
1235
1250
|
PolicySetListItem,
|
|
1236
1251
|
AlertResponse,
|
|
1252
|
+
BulkUpdateAlertStatusResponse,
|
|
1237
1253
|
AlertStatsResponse,
|
|
1238
1254
|
BillingSummaryResponse,
|
|
1239
1255
|
UsageResponse,
|
|
@@ -1388,7 +1404,8 @@ var CampaignGroupSchema = schemas.CampaignGroupResponse.pick({
|
|
|
1388
1404
|
is_archived: true,
|
|
1389
1405
|
schedule_paused: true,
|
|
1390
1406
|
campaign_count: true,
|
|
1391
|
-
created_at: true
|
|
1407
|
+
created_at: true,
|
|
1408
|
+
last_run_at: true
|
|
1392
1409
|
}).strip();
|
|
1393
1410
|
var parseCampaignGroup = (raw) => parseWithSchema(CampaignGroupSchema, raw, "campaign-group");
|
|
1394
1411
|
var parseCampaignGroupArray = (raw) => parseArrayOrItemsWithSchema(CampaignGroupSchema, raw, "campaign-groups");
|
|
@@ -1600,6 +1617,11 @@ var AlertStatsSchema = schemas.AlertStatsResponse.pick({
|
|
|
1600
1617
|
dismissed: true
|
|
1601
1618
|
}).strip();
|
|
1602
1619
|
var parseAlertStats = (raw) => parseWithSchema(AlertStatsSchema, raw, "alert-stats");
|
|
1620
|
+
var BulkUpdateAlertStatusSchema = schemas.BulkUpdateAlertStatusResponse.pick({
|
|
1621
|
+
updated: true,
|
|
1622
|
+
skipped: true
|
|
1623
|
+
}).strip();
|
|
1624
|
+
var parseBulkUpdateAlertStatus = (raw) => parseWithSchema(BulkUpdateAlertStatusSchema, raw, "bulk-update-alert-status");
|
|
1603
1625
|
var UsageSchema = schemas.UsageResponse.pick({
|
|
1604
1626
|
id: true,
|
|
1605
1627
|
scan_id: true,
|
|
@@ -1724,6 +1746,14 @@ var GeoSchema = schemas.GeoResponse.pick({
|
|
|
1724
1746
|
var GeoListSchema = z.array(GeoSchema);
|
|
1725
1747
|
var parseGeoList = (raw) => parseWithSchema(GeoListSchema, raw, "geos");
|
|
1726
1748
|
|
|
1749
|
+
// src/infrastructure/api/parsers/parse-linked-campaign-page.ts
|
|
1750
|
+
var LinkedCampaignSchema = schemas.LinkedCampaignResponse.pick({
|
|
1751
|
+
id: true,
|
|
1752
|
+
name: true,
|
|
1753
|
+
is_archived: true
|
|
1754
|
+
}).strip();
|
|
1755
|
+
var parseLinkedCampaignPage = (raw) => parsePagedWithItemSchema(LinkedCampaignSchema, raw, "policy-set-campaigns");
|
|
1756
|
+
|
|
1727
1757
|
// src/infrastructure/api/parsers/parse-policy-set.ts
|
|
1728
1758
|
var PolicySetSchema = schemas.PolicySetResponse.pick({
|
|
1729
1759
|
id: true,
|
|
@@ -1979,7 +2009,50 @@ function createHttpApiGateway(config) {
|
|
|
1979
2009
|
}
|
|
1980
2010
|
return err(toApiError(status2, error ?? data, response.headers.get("retry-after") ?? void 0));
|
|
1981
2011
|
}
|
|
1982
|
-
async function
|
|
2012
|
+
async function readCapped(response, maxBytes) {
|
|
2013
|
+
const tooLarge = err({
|
|
2014
|
+
kind: "upstream",
|
|
2015
|
+
detail: `Artifact is larger than the ${formatBytes(maxBytes)} this tool will transfer. Open the report URL from \`get_scan\` to view it instead.`
|
|
2016
|
+
});
|
|
2017
|
+
const body = response.body;
|
|
2018
|
+
if (body === null) {
|
|
2019
|
+
return err({ kind: "upstream", detail: "Artifact response had no body." });
|
|
2020
|
+
}
|
|
2021
|
+
const declared = Number(response.headers.get("content-length") ?? Number.NaN);
|
|
2022
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
2023
|
+
await body.cancel().catch(() => void 0);
|
|
2024
|
+
return tooLarge;
|
|
2025
|
+
}
|
|
2026
|
+
const reader = body.getReader();
|
|
2027
|
+
const chunks = [];
|
|
2028
|
+
let total = 0;
|
|
2029
|
+
try {
|
|
2030
|
+
for (; ; ) {
|
|
2031
|
+
const { done, value } = await reader.read();
|
|
2032
|
+
if (done) break;
|
|
2033
|
+
total += value.byteLength;
|
|
2034
|
+
if (total > maxBytes) {
|
|
2035
|
+
await reader.cancel().catch(() => void 0);
|
|
2036
|
+
return tooLarge;
|
|
2037
|
+
}
|
|
2038
|
+
chunks.push(value);
|
|
2039
|
+
}
|
|
2040
|
+
} catch (cause) {
|
|
2041
|
+
await reader.cancel().catch(() => void 0);
|
|
2042
|
+
return err({
|
|
2043
|
+
kind: "upstream",
|
|
2044
|
+
detail: cause instanceof Error ? cause.message : "artifact stream failed"
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
const out = new Uint8Array(total);
|
|
2048
|
+
let offset = 0;
|
|
2049
|
+
for (const chunk of chunks) {
|
|
2050
|
+
out.set(chunk, offset);
|
|
2051
|
+
offset += chunk.byteLength;
|
|
2052
|
+
}
|
|
2053
|
+
return ok(out);
|
|
2054
|
+
}
|
|
2055
|
+
async function binaryGet(path, maxBytes, query) {
|
|
1983
2056
|
const startedAtMs = Date.now();
|
|
1984
2057
|
const qs = query && Object.keys(query).length > 0 ? `?${new URLSearchParams(
|
|
1985
2058
|
Object.fromEntries(Object.entries(query).map(([k, v]) => [k, String(v)]))
|
|
@@ -1994,7 +2067,7 @@ function createHttpApiGateway(config) {
|
|
|
1994
2067
|
// update + a tenant-isolation review.
|
|
1995
2068
|
headers: {
|
|
1996
2069
|
authorization: bearer.toAuthorizationHeader(),
|
|
1997
|
-
accept: "image/*, application/pdf, application/octet-stream",
|
|
2070
|
+
accept: "image/*, application/pdf, video/mp4, text/plain, application/xml, text/xml, application/octet-stream",
|
|
1998
2071
|
"user-agent": "kaminari-ad-mcp",
|
|
1999
2072
|
"x-request-id": requestId
|
|
2000
2073
|
}
|
|
@@ -2012,9 +2085,10 @@ function createHttpApiGateway(config) {
|
|
|
2012
2085
|
"api.done"
|
|
2013
2086
|
);
|
|
2014
2087
|
if (status2 >= 200 && status2 < 300) {
|
|
2015
|
-
const
|
|
2088
|
+
const read = await readCapped(response, maxBytes);
|
|
2089
|
+
if (read.isErr()) return err(read.error);
|
|
2016
2090
|
return ok({
|
|
2017
|
-
bytes:
|
|
2091
|
+
bytes: read.value,
|
|
2018
2092
|
contentType: response.headers.get("content-type") ?? "application/octet-stream"
|
|
2019
2093
|
});
|
|
2020
2094
|
}
|
|
@@ -2077,22 +2151,37 @@ function createHttpApiGateway(config) {
|
|
|
2077
2151
|
return call("PUT", "/api/v1/account/labels", { body }, parseArrayOf(parseLabelDefinition));
|
|
2078
2152
|
},
|
|
2079
2153
|
async getScanScreenshot(scanId, w2) {
|
|
2080
|
-
return binaryGet(
|
|
2154
|
+
return binaryGet(
|
|
2155
|
+
`/api/v1/scans/${scanId}/screenshot`,
|
|
2156
|
+
MAX_BINARY_ARTIFACT_BYTES,
|
|
2157
|
+
w2 !== void 0 ? { w: w2 } : void 0
|
|
2158
|
+
);
|
|
2081
2159
|
},
|
|
2082
2160
|
async getScanCreativeScreenshot(scanId, w2) {
|
|
2083
2161
|
return binaryGet(
|
|
2084
2162
|
`/api/v1/scans/${scanId}/creative-screenshot`,
|
|
2163
|
+
MAX_BINARY_ARTIFACT_BYTES,
|
|
2085
2164
|
w2 !== void 0 ? { w: w2 } : void 0
|
|
2086
2165
|
);
|
|
2087
2166
|
},
|
|
2088
2167
|
async getScanLandingScreenshot(scanId, landingOrd, w2) {
|
|
2089
2168
|
return binaryGet(
|
|
2090
2169
|
`/api/v1/scans/${scanId}/landings/${String(landingOrd)}/screenshot`,
|
|
2170
|
+
MAX_BINARY_ARTIFACT_BYTES,
|
|
2091
2171
|
w2 !== void 0 ? { w: w2 } : void 0
|
|
2092
2172
|
);
|
|
2093
2173
|
},
|
|
2174
|
+
async getScanCreativeHtml(scanId) {
|
|
2175
|
+
return binaryGet(`/api/v1/scans/${scanId}/creative-html`, MAX_TEXT_ARTIFACT_BYTES);
|
|
2176
|
+
},
|
|
2177
|
+
async getScanCreativeVideo(scanId) {
|
|
2178
|
+
return binaryGet(`/api/v1/scans/${scanId}/creative-video`, MAX_BINARY_ARTIFACT_BYTES);
|
|
2179
|
+
},
|
|
2180
|
+
async getScanVastXml(scanId) {
|
|
2181
|
+
return binaryGet(`/api/v1/scans/${scanId}/vast-xml`, MAX_TEXT_ARTIFACT_BYTES);
|
|
2182
|
+
},
|
|
2094
2183
|
async getInvoicePdf(invoiceId) {
|
|
2095
|
-
return binaryGet(`/api/v1/invoices/${invoiceId}/pdf
|
|
2184
|
+
return binaryGet(`/api/v1/invoices/${invoiceId}/pdf`, MAX_BINARY_ARTIFACT_BYTES);
|
|
2096
2185
|
},
|
|
2097
2186
|
async listApiKeys() {
|
|
2098
2187
|
return call("GET", "/api/v1/account/api-keys", {}, parseApiKeyList);
|
|
@@ -2435,6 +2524,30 @@ function createHttpApiGateway(config) {
|
|
|
2435
2524
|
parseEmpty
|
|
2436
2525
|
);
|
|
2437
2526
|
},
|
|
2527
|
+
async listPolicySetCampaigns(id, filters) {
|
|
2528
|
+
return call(
|
|
2529
|
+
"GET",
|
|
2530
|
+
"/api/v1/policy-sets/{policy_set_id}/campaigns",
|
|
2531
|
+
{ params: { path: { policy_set_id: id }, query: filters } },
|
|
2532
|
+
parseLinkedCampaignPage
|
|
2533
|
+
);
|
|
2534
|
+
},
|
|
2535
|
+
async attachPolicySetCampaigns(id, body) {
|
|
2536
|
+
return call(
|
|
2537
|
+
"POST",
|
|
2538
|
+
"/api/v1/policy-sets/{policy_set_id}/campaigns/attach",
|
|
2539
|
+
{ params: { path: { policy_set_id: id } }, body },
|
|
2540
|
+
parseEmpty
|
|
2541
|
+
);
|
|
2542
|
+
},
|
|
2543
|
+
async detachPolicySetCampaigns(id, body) {
|
|
2544
|
+
return call(
|
|
2545
|
+
"POST",
|
|
2546
|
+
"/api/v1/policy-sets/{policy_set_id}/campaigns/detach",
|
|
2547
|
+
{ params: { path: { policy_set_id: id } }, body },
|
|
2548
|
+
parseEmpty
|
|
2549
|
+
);
|
|
2550
|
+
},
|
|
2438
2551
|
async requestPolicySetApproval(id) {
|
|
2439
2552
|
return call(
|
|
2440
2553
|
"POST",
|
|
@@ -2444,8 +2557,13 @@ function createHttpApiGateway(config) {
|
|
|
2444
2557
|
);
|
|
2445
2558
|
},
|
|
2446
2559
|
// ── Custom taxonomies ─────────────────────────────────────────
|
|
2447
|
-
async listCustomTaxonomies() {
|
|
2448
|
-
return call(
|
|
2560
|
+
async listCustomTaxonomies(filters) {
|
|
2561
|
+
return call(
|
|
2562
|
+
"GET",
|
|
2563
|
+
"/api/v1/custom-taxonomies",
|
|
2564
|
+
{ params: { query: filters ?? {} } },
|
|
2565
|
+
parseCustomTaxonomyList
|
|
2566
|
+
);
|
|
2449
2567
|
},
|
|
2450
2568
|
async getCustomTaxonomy(id) {
|
|
2451
2569
|
return call(
|
|
@@ -2502,8 +2620,11 @@ function createHttpApiGateway(config) {
|
|
|
2502
2620
|
parseEmpty
|
|
2503
2621
|
);
|
|
2504
2622
|
},
|
|
2505
|
-
async
|
|
2506
|
-
return call("
|
|
2623
|
+
async bulkUpdateAlertStatus(body) {
|
|
2624
|
+
return call("POST", "/api/v1/alerts/bulk-status", { body }, parseBulkUpdateAlertStatus);
|
|
2625
|
+
},
|
|
2626
|
+
async getAlertStats(filters) {
|
|
2627
|
+
return call("GET", "/api/v1/alerts/stats", { params: { query: filters } }, parseAlertStats);
|
|
2507
2628
|
},
|
|
2508
2629
|
// ── Webhooks ──────────────────────────────────────────────────
|
|
2509
2630
|
async listWebhooks() {
|
|
@@ -3107,7 +3228,7 @@ var GetCampaignAlertOverridesInputShape = {
|
|
|
3107
3228
|
};
|
|
3108
3229
|
var getCampaignAlertOverridesTool = {
|
|
3109
3230
|
name: "get_campaign_alert_overrides",
|
|
3110
|
-
description: "Get the per-campaign override of which alert destinations receive its alerts. `mode` is one of `inherit` (
|
|
3231
|
+
description: "Get the per-campaign override of which alert destinations receive its alerts. `mode` is one of `inherit` (no override \u2014 the campaign follows the org-wide destinations), `override` (alerts go ONLY to the returned `destination_ids`), or `silence` (the campaign sends nothing).",
|
|
3111
3232
|
annotations: {
|
|
3112
3233
|
title: "Get Campaign Alert Overrides",
|
|
3113
3234
|
readOnlyHint: true,
|
|
@@ -3167,16 +3288,21 @@ var setAlertDestinationVersionTool = {
|
|
|
3167
3288
|
};
|
|
3168
3289
|
var SetCampaignAlertOverridesInputShape = {
|
|
3169
3290
|
campaign_id: z.string().uuid().describe("Campaign UUID."),
|
|
3170
|
-
|
|
3171
|
-
|
|
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(
|
|
3297
|
+
"Routing mode: `inherit` (fall back to the org-wide destinations), `override` (route ONLY to `destination_ids`), `silence` (send nothing for this campaign)."
|
|
3172
3298
|
),
|
|
3173
3299
|
destination_ids: z.array(z.string().uuid()).max(50).default([]).describe(
|
|
3174
|
-
"Destination UUIDs
|
|
3300
|
+
"Destination UUIDs to route to. Accepted ONLY with `mode: override` \u2014 the API rejects it for `inherit`/`silence`. An empty list with `override` routes nowhere, which is the same outcome as `silence`."
|
|
3175
3301
|
)
|
|
3176
3302
|
};
|
|
3177
3303
|
var setCampaignAlertOverridesTool = {
|
|
3178
3304
|
name: "set_campaign_alert_overrides",
|
|
3179
|
-
description: "REPLACE the per-campaign alert-routing override. `mode=inherit`
|
|
3305
|
+
description: "REPLACE the per-campaign alert-routing override. `mode=inherit` drops the override so the campaign follows the org-wide destinations; `mode=override` routes its alerts ONLY to `destination_ids`; `mode=silence` sends nothing for the campaign. There is no 'route everywhere except these' mode \u2014 to exclude one destination, pass `override` with the destinations you DO want (see `list_alert_destinations`). To read the new state, follow up with `get_campaign_alert_overrides`.",
|
|
3180
3306
|
annotations: {
|
|
3181
3307
|
title: "Set Campaign Alert Overrides",
|
|
3182
3308
|
readOnlyHint: false,
|
|
@@ -3186,6 +3312,13 @@ var setCampaignAlertOverridesTool = {
|
|
|
3186
3312
|
},
|
|
3187
3313
|
inputSchema: z.object(SetCampaignAlertOverridesInputShape),
|
|
3188
3314
|
handler: async (input, ctx) => {
|
|
3315
|
+
if (input.mode !== "override" && input.destination_ids.length > 0) {
|
|
3316
|
+
return err({
|
|
3317
|
+
kind: "invalid-input",
|
|
3318
|
+
message: `destination_ids is only accepted with mode "override", got "${input.mode}". Use mode "override" to route to specific destinations.`,
|
|
3319
|
+
fieldErrors: { destination_ids: ["only valid with mode 'override'"] }
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3189
3322
|
const result = await ctx.api.setCampaignAlertOverrides(input.campaign_id, {
|
|
3190
3323
|
mode: input.mode,
|
|
3191
3324
|
destination_ids: input.destination_ids
|
|
@@ -3194,10 +3327,116 @@ var setCampaignAlertOverridesTool = {
|
|
|
3194
3327
|
return ok({ updated: true });
|
|
3195
3328
|
}
|
|
3196
3329
|
};
|
|
3197
|
-
var
|
|
3330
|
+
var MAX_IDS = 1e3;
|
|
3331
|
+
var FILTER_FIELDS = [
|
|
3332
|
+
"filter_status",
|
|
3333
|
+
"filter_campaign_id",
|
|
3334
|
+
"filter_policy_set_ids",
|
|
3335
|
+
"filter_tag_slugs",
|
|
3336
|
+
"filter_country_codes",
|
|
3337
|
+
"filter_date_from",
|
|
3338
|
+
"filter_date_to",
|
|
3339
|
+
"filter_timezone"
|
|
3340
|
+
];
|
|
3341
|
+
var BulkUpdateAlertStatusInputShape = {
|
|
3342
|
+
status: z.enum(["open", "escalated", "resolved", "dismissed"]).describe("Status to move the selected alerts to."),
|
|
3343
|
+
ids: z.array(z.string().uuid()).min(1).max(MAX_IDS).optional().describe(
|
|
3344
|
+
`Explicit alert UUIDs, at most ${String(MAX_IDS)} per call. Mutually exclusive with \`all_matching\`.`
|
|
3345
|
+
),
|
|
3346
|
+
all_matching: z.boolean().optional().describe(
|
|
3347
|
+
"Apply to every alert matching the `filter_*` fields instead of an id list. With no filters this means EVERY alert in the organization, so filter first and confirm the scope with `get_alert_stats` using the same filters."
|
|
3348
|
+
),
|
|
3349
|
+
filter_status: z.enum(["open", "escalated", "resolved", "dismissed"]).optional().describe("Only with `all_matching`: restrict to alerts currently in this status."),
|
|
3350
|
+
filter_campaign_id: z.string().uuid().optional().describe("Only with `all_matching`: restrict to one campaign."),
|
|
3351
|
+
filter_policy_set_ids: z.array(z.string().uuid()).min(1).optional().describe("Only with `all_matching`: restrict to alerts raised by any of these policy sets."),
|
|
3352
|
+
filter_tag_slugs: z.array(z.string().min(1)).min(1).optional().describe("Only with `all_matching`: restrict to alerts carrying any of these tag slugs."),
|
|
3353
|
+
filter_country_codes: z.array(z.string().min(2).max(2)).min(1).optional().describe("Only with `all_matching`: restrict to these ISO 3166-1 alpha-2 countries."),
|
|
3354
|
+
filter_date_from: z.string().date().optional().describe("Only with `all_matching`: inclusive lower bound (YYYY-MM-DD) on alert creation."),
|
|
3355
|
+
filter_date_to: z.string().date().optional().describe("Only with `all_matching`: inclusive upper bound (YYYY-MM-DD) on alert creation."),
|
|
3356
|
+
filter_timezone: z.string().optional().describe(
|
|
3357
|
+
"IANA timezone for the filter date bounds. REQUIRED by the API whenever either date is set."
|
|
3358
|
+
)
|
|
3359
|
+
};
|
|
3360
|
+
var bulkUpdateAlertStatusTool = {
|
|
3361
|
+
name: "bulk_update_alert_status",
|
|
3362
|
+
description: "Change the status of many alerts at once. Select EITHER an explicit `ids` list (max 1000) OR `all_matching: true` with the `filter_*` fields, which mirror `list_alerts`. Alerts already in a state the transition cannot leave are skipped rather than failing the call, so the response reports `updated` and `skipped` counts \u2014 compare `updated + skipped` against the selection size to see how many were no-ops. Prefer `update_alert_status` for a single alert.",
|
|
3363
|
+
annotations: {
|
|
3364
|
+
title: "Bulk Update Alert Status",
|
|
3365
|
+
readOnlyHint: false,
|
|
3366
|
+
destructiveHint: false,
|
|
3367
|
+
idempotentHint: true,
|
|
3368
|
+
openWorldHint: false
|
|
3369
|
+
},
|
|
3370
|
+
inputSchema: z.object(BulkUpdateAlertStatusInputShape),
|
|
3371
|
+
handler: async (input, ctx) => {
|
|
3372
|
+
const matchesAll = input.all_matching === true;
|
|
3373
|
+
if (input.ids !== void 0 === matchesAll) {
|
|
3374
|
+
return err({
|
|
3375
|
+
kind: "invalid-input",
|
|
3376
|
+
message: "Select alerts with either `ids` or `all_matching: true` \u2014 not both, and not neither."
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
const suppliedFilters = FILTER_FIELDS.filter((field) => input[field] !== void 0);
|
|
3380
|
+
if (!matchesAll && suppliedFilters.length > 0) {
|
|
3381
|
+
return err({
|
|
3382
|
+
kind: "invalid-input",
|
|
3383
|
+
message: `\`${suppliedFilters.join("`, `")}\` applies only with \`all_matching: true\`. An explicit \`ids\` list is already the selection, so the filters would be ignored.`
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
if ((input.filter_date_from !== void 0 || input.filter_date_to !== void 0) && input.filter_timezone === void 0) {
|
|
3387
|
+
return err({
|
|
3388
|
+
kind: "invalid-input",
|
|
3389
|
+
message: "filter_timezone is required whenever filter_date_from or filter_date_to is set.",
|
|
3390
|
+
fieldErrors: { filter_timezone: ["required alongside a filter date bound"] }
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
const result = await ctx.api.bulkUpdateAlertStatus({
|
|
3394
|
+
status: input.status,
|
|
3395
|
+
all_matching: matchesAll,
|
|
3396
|
+
...input.ids !== void 0 ? { ids: [...input.ids] } : {},
|
|
3397
|
+
...input.filter_status !== void 0 ? { filter_status: input.filter_status } : {},
|
|
3398
|
+
...input.filter_campaign_id !== void 0 ? { filter_campaign_id: input.filter_campaign_id } : {},
|
|
3399
|
+
...input.filter_policy_set_ids !== void 0 ? { filter_policy_set_ids: [...input.filter_policy_set_ids] } : {},
|
|
3400
|
+
...input.filter_tag_slugs !== void 0 ? { filter_tag_slugs: [...input.filter_tag_slugs] } : {},
|
|
3401
|
+
...input.filter_country_codes !== void 0 ? { filter_country_codes: [...input.filter_country_codes] } : {},
|
|
3402
|
+
...input.filter_date_from !== void 0 ? { filter_date_from: input.filter_date_from } : {},
|
|
3403
|
+
...input.filter_date_to !== void 0 ? { filter_date_to: input.filter_date_to } : {},
|
|
3404
|
+
...input.filter_timezone !== void 0 ? { filter_timezone: input.filter_timezone } : {}
|
|
3405
|
+
});
|
|
3406
|
+
if (result.isErr()) return err(mapApiError(result.error));
|
|
3407
|
+
return ok(result.value);
|
|
3408
|
+
}
|
|
3409
|
+
};
|
|
3410
|
+
var alertFilterFields = {
|
|
3411
|
+
campaign_id: z.string().uuid().optional().describe("Filter to one campaign's alerts."),
|
|
3412
|
+
policy_set_id: z.string().optional().describe(
|
|
3413
|
+
"Comma-separated policy-set UUIDs. Matches alerts raised by any of them; see `list_policy_sets`."
|
|
3414
|
+
),
|
|
3415
|
+
tag: z.string().optional().describe("Comma-separated tag slugs. Matches alerts carrying any of them; see `list_tags`."),
|
|
3416
|
+
country_code: z.string().optional().describe("Comma-separated ISO 3166-1 alpha-2 country codes, e.g. US,DE,JP."),
|
|
3417
|
+
date_from: z.string().date().optional().describe("ISO date (YYYY-MM-DD), inclusive lower bound on alert creation."),
|
|
3418
|
+
date_to: z.string().date().optional().describe("ISO date (YYYY-MM-DD), inclusive upper bound on alert creation."),
|
|
3419
|
+
timezone: z.string().optional().describe(
|
|
3420
|
+
"IANA timezone (e.g. 'Europe/Berlin') the date bounds are read in. Defaults to UTC. Omitting both dates spans all time."
|
|
3421
|
+
)
|
|
3422
|
+
};
|
|
3423
|
+
function toAlertFilterQuery(input) {
|
|
3424
|
+
return {
|
|
3425
|
+
...input.campaign_id !== void 0 ? { campaign_id: input.campaign_id } : {},
|
|
3426
|
+
...input.policy_set_id !== void 0 ? { policy_set_id: input.policy_set_id } : {},
|
|
3427
|
+
...input.tag !== void 0 ? { tag: input.tag } : {},
|
|
3428
|
+
...input.country_code !== void 0 ? { country_code: input.country_code } : {},
|
|
3429
|
+
...input.date_from !== void 0 ? { date_from: input.date_from } : {},
|
|
3430
|
+
...input.date_to !== void 0 ? { date_to: input.date_to } : {},
|
|
3431
|
+
...input.timezone !== void 0 ? { timezone: input.timezone } : {}
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
// src/application/tools/alerts/get-alert-stats.tool.ts
|
|
3436
|
+
var GetAlertStatsInputShape = alertFilterFields;
|
|
3198
3437
|
var getAlertStatsTool = {
|
|
3199
3438
|
name: "get_alert_stats",
|
|
3200
|
-
description: "Get alert counts grouped by status: open, escalated, resolved, dismissed.
|
|
3439
|
+
description: "Get alert counts grouped by status: open, escalated, resolved, dismissed. Accepts every filter `list_alerts` does except `status` \u2014 the response buckets BY status \u2014 so passing the same filters to both makes the four counts sum to the `total` that `list_alerts` reports. Unfiltered, the counts cover the whole organization over all time. Use this to size a selection before calling `bulk_update_alert_status` with `all_matching`.",
|
|
3201
3440
|
annotations: {
|
|
3202
3441
|
title: "Get Alert Stats",
|
|
3203
3442
|
readOnlyHint: true,
|
|
@@ -3206,14 +3445,14 @@ var getAlertStatsTool = {
|
|
|
3206
3445
|
openWorldHint: false
|
|
3207
3446
|
},
|
|
3208
3447
|
inputSchema: z.object(GetAlertStatsInputShape),
|
|
3209
|
-
handler: async (
|
|
3210
|
-
const result = await ctx.api.getAlertStats();
|
|
3448
|
+
handler: async (input, ctx) => {
|
|
3449
|
+
const result = await ctx.api.getAlertStats(toAlertFilterQuery(input));
|
|
3211
3450
|
if (result.isErr()) return err(mapApiError(result.error));
|
|
3212
3451
|
return ok(result.value);
|
|
3213
3452
|
}
|
|
3214
3453
|
};
|
|
3215
3454
|
var ListAlertsInputShape = {
|
|
3216
|
-
|
|
3455
|
+
...alertFilterFields,
|
|
3217
3456
|
status: z.enum(["open", "escalated", "resolved", "dismissed"]).optional().describe(
|
|
3218
3457
|
"Filter by alert status. Open = unhandled; escalated = flagged for attention; resolved = closed; dismissed = closed without action."
|
|
3219
3458
|
),
|
|
@@ -3222,7 +3461,7 @@ var ListAlertsInputShape = {
|
|
|
3222
3461
|
};
|
|
3223
3462
|
var listAlertsTool = {
|
|
3224
3463
|
name: "list_alerts",
|
|
3225
|
-
description: "List violation alerts (one per scan + violating rule combo) with offer URL, country, status, scan back-reference, and the kind-aware fields `rule_type` (tag / iab_v3 / brand / ai_category / custom_taxonomy) + `matched_value` (the canonical text the scan matched against).",
|
|
3464
|
+
description: "List violation alerts (one per scan + violating rule combo) with offer URL, country, status, scan back-reference, and the kind-aware fields `rule_type` (tag / iab_v3 / brand / ai_category / custom_taxonomy) + `matched_value` (the canonical text the scan matched against). Filter by campaign, status, policy set, tag, country and creation-date range; `policy_set_id`, `tag` and `country_code` take comma-separated values and match any of them. Pass the same filters to `get_alert_stats` to get counts that agree with this list.",
|
|
3226
3465
|
annotations: {
|
|
3227
3466
|
title: "List Alerts",
|
|
3228
3467
|
readOnlyHint: true,
|
|
@@ -3235,7 +3474,7 @@ var listAlertsTool = {
|
|
|
3235
3474
|
const filters = {
|
|
3236
3475
|
page: input.page,
|
|
3237
3476
|
limit: input.limit,
|
|
3238
|
-
...input
|
|
3477
|
+
...toAlertFilterQuery(input),
|
|
3239
3478
|
...input.status !== void 0 ? { status: input.status } : {}
|
|
3240
3479
|
};
|
|
3241
3480
|
const result = await ctx.api.listAlerts(filters);
|
|
@@ -3458,12 +3697,34 @@ var getCampaignGroupTool = {
|
|
|
3458
3697
|
return ok(result.value);
|
|
3459
3698
|
}
|
|
3460
3699
|
};
|
|
3700
|
+
var campaignDateFilterFields = {
|
|
3701
|
+
created_from: z.string().date().optional().describe("ISO date (YYYY-MM-DD), inclusive lower bound on the creation day."),
|
|
3702
|
+
created_to: z.string().date().optional().describe("ISO date (YYYY-MM-DD), inclusive upper bound on the creation day."),
|
|
3703
|
+
last_run_from: z.string().date().optional().describe(
|
|
3704
|
+
"ISO date (YYYY-MM-DD), inclusive lower bound on the most recent run. Setting either last-run bound excludes anything that has never run."
|
|
3705
|
+
),
|
|
3706
|
+
last_run_to: z.string().date().optional().describe("ISO date (YYYY-MM-DD), inclusive upper bound on the most recent run."),
|
|
3707
|
+
timezone: z.string().optional().describe("IANA timezone (e.g. 'Europe/Berlin') the date bounds are read in. Defaults to UTC.")
|
|
3708
|
+
};
|
|
3709
|
+
function toCampaignDateFilterQuery(input) {
|
|
3710
|
+
return {
|
|
3711
|
+
...input.created_from !== void 0 ? { created_from: input.created_from } : {},
|
|
3712
|
+
...input.created_to !== void 0 ? { created_to: input.created_to } : {},
|
|
3713
|
+
...input.last_run_from !== void 0 ? { last_run_from: input.last_run_from } : {},
|
|
3714
|
+
...input.last_run_to !== void 0 ? { last_run_to: input.last_run_to } : {},
|
|
3715
|
+
...input.timezone !== void 0 ? { timezone: input.timezone } : {}
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// src/application/tools/campaign-groups/list-campaign-groups.tool.ts
|
|
3461
3720
|
var ListCampaignGroupsInputShape = {
|
|
3462
|
-
archived: z.boolean().optional().describe("If true, list ONLY archived groups. Default: only active groups.")
|
|
3721
|
+
archived: z.boolean().optional().describe("If true, list ONLY archived groups. Default: only active groups."),
|
|
3722
|
+
q: z.string().min(1).max(200).optional().describe("Substring search against group name (case-insensitive)."),
|
|
3723
|
+
...campaignDateFilterFields
|
|
3463
3724
|
};
|
|
3464
3725
|
var listCampaignGroupsTool = {
|
|
3465
3726
|
name: "list_campaign_groups",
|
|
3466
|
-
description: "List campaign groups \u2014 folders that group related campaigns
|
|
3727
|
+
description: "List campaign groups \u2014 folders that group related campaigns \u2014 with per-group campaign count and `last_run_at`. Filter by archived flag, name substring, or creation / last-run date range; a group's last run is the newest run across its campaigns. Not paginated; the org-scoped list is typically small (a few dozen groups max).",
|
|
3467
3728
|
annotations: {
|
|
3468
3729
|
title: "List Campaign Groups",
|
|
3469
3730
|
readOnlyHint: true,
|
|
@@ -3473,7 +3734,11 @@ var listCampaignGroupsTool = {
|
|
|
3473
3734
|
},
|
|
3474
3735
|
inputSchema: z.object(ListCampaignGroupsInputShape),
|
|
3475
3736
|
handler: async (input, ctx) => {
|
|
3476
|
-
const filters =
|
|
3737
|
+
const filters = {
|
|
3738
|
+
...toCampaignDateFilterQuery(input),
|
|
3739
|
+
...input.archived !== void 0 ? { archived: input.archived } : {},
|
|
3740
|
+
...input.q !== void 0 ? { q: input.q } : {}
|
|
3741
|
+
};
|
|
3477
3742
|
const result = await ctx.api.listCampaignGroups(filters);
|
|
3478
3743
|
if (result.isErr()) return err(mapApiError(result.error));
|
|
3479
3744
|
return ok({ items: result.value });
|
|
@@ -3798,12 +4063,13 @@ var ListCampaignsInputShape = {
|
|
|
3798
4063
|
"When omitted, the API returns active campaigns only. Pass `true` to see archived campaigns, `false` for active-only (explicit)."
|
|
3799
4064
|
),
|
|
3800
4065
|
q: z.string().min(1).max(200).optional().describe("Substring search against campaign name (case-insensitive)."),
|
|
4066
|
+
...campaignDateFilterFields,
|
|
3801
4067
|
page: z.number().int().min(1).max(500).default(1).describe("1-indexed page number."),
|
|
3802
4068
|
limit: z.number().int().min(1).max(200).default(50).describe("Page size (1-200).")
|
|
3803
4069
|
};
|
|
3804
4070
|
var listCampaignsTool = {
|
|
3805
4071
|
name: "list_campaigns",
|
|
3806
|
-
description: "List campaigns for the caller's organization, optionally filtered by group, archived flag, or
|
|
4072
|
+
description: "List campaigns for the caller's organization, optionally filtered by group, archived flag, name substring, or creation / last-run date range. Paginated. Each row carries the same fields as `get_campaign`, including the repeat / retry settings (`repeat_count`, `repeat_mode`, `retry_max_attempts`) \u2014 use them to spot the campaigns that multiply their per-run scan count.",
|
|
3807
4073
|
annotations: {
|
|
3808
4074
|
title: "List Campaigns",
|
|
3809
4075
|
readOnlyHint: true,
|
|
@@ -3816,6 +4082,7 @@ var listCampaignsTool = {
|
|
|
3816
4082
|
const filters = {
|
|
3817
4083
|
page: input.page,
|
|
3818
4084
|
limit: input.limit,
|
|
4085
|
+
...toCampaignDateFilterQuery(input),
|
|
3819
4086
|
...input.group_id !== void 0 ? { group_id: input.group_id } : {},
|
|
3820
4087
|
...input.archived !== void 0 ? { archived: input.archived } : {},
|
|
3821
4088
|
...input.q !== void 0 ? { q: input.q } : {}
|
|
@@ -3944,19 +4211,47 @@ var updateCampaignTool = {
|
|
|
3944
4211
|
return ok(result.value);
|
|
3945
4212
|
}
|
|
3946
4213
|
};
|
|
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."),
|
|
4216
|
+
flags: z.enum(["", "i"]).optional().describe("Omit or use '' for case-sensitive matching; use 'i' to ignore case.")
|
|
4217
|
+
}).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;
|
|
4221
|
+
if (input.target !== void 0 && input.target !== "page") {
|
|
4222
|
+
return {
|
|
4223
|
+
kind: "invalid-input",
|
|
4224
|
+
message: "regexp_request_url requires target='page'.",
|
|
4225
|
+
fieldErrors: { target: ["Must be 'page' for regexp_request_url."] }
|
|
4226
|
+
};
|
|
4227
|
+
}
|
|
4228
|
+
const parsed = requestUrlRuleConfigSchema.safeParse(input.config);
|
|
4229
|
+
if (parsed.success) return null;
|
|
4230
|
+
const issue = parsed.error.issues[0];
|
|
4231
|
+
const field = issue?.path.length ? `config.${issue.path.join(".")}` : "config";
|
|
4232
|
+
const message = issue?.message ?? "Invalid request-URL rule config.";
|
|
4233
|
+
return {
|
|
4234
|
+
kind: "invalid-input",
|
|
4235
|
+
message: `Invalid regexp_request_url config: ${message}`,
|
|
4236
|
+
fieldErrors: { [field]: [message] }
|
|
4237
|
+
};
|
|
4238
|
+
}
|
|
3947
4239
|
var COMBO_MATCH_SCOPE_DOC = 'For `rule_type=\'combo\'` the optional rule-level key `match_scope` decides where conditions are counted. `"scan"` (the default, and the behaviour when the key is absent) checks them against the union of all tags on the scan. With `"url"`, every condition must be satisfied by tags on the same link, and the tag is assigned to that link \u2014 so a rule built only from link-less tags (AI verdicts, crawler behaviour) has no link to attach to and will never match; keep those on the whole scan. Mixing the two kinds does not help either: AI verdicts and per-link detections reach the engine in different scan parts and are not evaluated together today, so such a combo fires under neither scope. Give a `"url"` rule at least one positive condition (`all_of` / `any_of` / `tag_category` + `count_gte`) \u2014 a config carrying only `none_of` matches every link that merely lacks those tags, and the API does not reject that on this path. Thresholds stay "N or more" (`count_gte` / `any_of_min`) in both scopes; no other `match_scope` value is accepted.';
|
|
3948
4240
|
var COMBO_MATCH_SCOPE_READ_DOC = 'For `rule_type=\'combo\'` the returned `config` may carry the rule-level key `match_scope` ("scan" = count conditions across the whole scan, "url" = every condition must be satisfied by tags on the same link). Resend it verbatim when updating: `update_custom_rule` replaces `config` wholesale, so dropping the key reverts the rule to whole-scan matching.';
|
|
3949
4241
|
var ruleConfigField = z.record(z.unknown()).superRefine((config, ctx) => {
|
|
3950
4242
|
const scope = config["match_scope"];
|
|
3951
|
-
if (scope
|
|
3952
|
-
|
|
4243
|
+
if (scope !== void 0 && scope !== "scan" && scope !== "url") {
|
|
4244
|
+
ctx.addIssue({
|
|
4245
|
+
code: z.ZodIssueCode.custom,
|
|
4246
|
+
path: ["match_scope"],
|
|
4247
|
+
message: 'config.match_scope must be "scan" or "url". Omit the key for scan-wide matching (the default).'
|
|
4248
|
+
});
|
|
3953
4249
|
}
|
|
3954
|
-
ctx.addIssue({
|
|
3955
|
-
code: z.ZodIssueCode.custom,
|
|
3956
|
-
path: ["match_scope"],
|
|
3957
|
-
message: 'config.match_scope must be "scan" or "url". Omit the key for scan-wide matching (the default).'
|
|
3958
|
-
});
|
|
3959
4250
|
});
|
|
4251
|
+
var requestUrlAwareRuleConfigField = z.union([
|
|
4252
|
+
requestUrlRuleConfigSchema,
|
|
4253
|
+
ruleConfigField
|
|
4254
|
+
]);
|
|
3960
4255
|
|
|
3961
4256
|
// src/application/tools/custom-rules/create-custom-rule.tool.ts
|
|
3962
4257
|
var CreateCustomRuleInputShape = {
|
|
@@ -3967,18 +4262,18 @@ var CreateCustomRuleInputShape = {
|
|
|
3967
4262
|
"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."
|
|
3968
4263
|
),
|
|
3969
4264
|
rule_type: z.string().max(50).describe(
|
|
3970
|
-
"Rule engine. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `blacklist_domain`, `combo`, `llm`. The API validates."
|
|
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."
|
|
3971
4266
|
),
|
|
3972
|
-
config:
|
|
3973
|
-
"Rule-type-specific configuration object. Shape depends on `rule_type`. For `rule_type='llm'` the shape is `{ prompt: string, tags: { <tag_slug>: <description>, ... } }`; each key in `config.tags` is auto-registered as a custom tag definition AND must not collide with a system slug (same 422 contract as `tag_slug`). " + COMBO_MATCH_SCOPE_DOC
|
|
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
|
|
3974
4269
|
),
|
|
3975
4270
|
target: z.string().max(30).optional().describe(
|
|
3976
|
-
"Where to apply the rule (e.g. 'page' for landing HTML). Default: page. See API docs for the full set of valid values."
|
|
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."
|
|
3977
4272
|
)
|
|
3978
4273
|
};
|
|
3979
4274
|
var createCustomRuleTool = {
|
|
3980
4275
|
name: "create_custom_rule",
|
|
3981
|
-
description: "Define a custom tag-detection 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`.",
|
|
3982
4277
|
annotations: {
|
|
3983
4278
|
title: "Create Custom Rule",
|
|
3984
4279
|
readOnlyHint: false,
|
|
@@ -3988,6 +4283,8 @@ var createCustomRuleTool = {
|
|
|
3988
4283
|
},
|
|
3989
4284
|
inputSchema: z.object(CreateCustomRuleInputShape),
|
|
3990
4285
|
handler: async (input, ctx) => {
|
|
4286
|
+
const inputError = requestUrlRuleInputError(input);
|
|
4287
|
+
if (inputError) return err(inputError);
|
|
3991
4288
|
const body = {
|
|
3992
4289
|
name: input.name,
|
|
3993
4290
|
rule_type: input.rule_type,
|
|
@@ -4061,19 +4358,19 @@ var listCustomRulesTool = {
|
|
|
4061
4358
|
};
|
|
4062
4359
|
var TestCustomRuleInputShape = {
|
|
4063
4360
|
rule_type: z.string().max(50).describe(
|
|
4064
|
-
"Rule engine type. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `blacklist_domain`, `combo`, `llm`."
|
|
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."
|
|
4065
4362
|
),
|
|
4066
|
-
config:
|
|
4067
|
-
"Rule-type-specific config to test. Same shape as `create_custom_rule`'s `config`. NOTE: `test_custom_rule` evaluates the rule against a scan WITHOUT persisting it, so slug-collision validation does NOT run here \u2014 verify slugs against `list_tags` (`scope=system`) before promoting to `create_custom_rule`. " + COMBO_MATCH_SCOPE_DOC
|
|
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
|
|
4068
4365
|
),
|
|
4069
4366
|
target: z.string().max(30).describe(
|
|
4070
|
-
"Where to apply the rule (e.g. 'page' for landing HTML). See API docs for the full set of valid values."
|
|
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."
|
|
4071
4368
|
),
|
|
4072
4369
|
scan_id: z.string().uuid().describe("Existing scan UUID to evaluate the rule against.")
|
|
4073
4370
|
};
|
|
4074
4371
|
var testCustomRuleTool = {
|
|
4075
4372
|
name: "test_custom_rule",
|
|
4076
|
-
description: "Preview-test a rule definition against
|
|
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.",
|
|
4077
4374
|
annotations: {
|
|
4078
4375
|
title: "Test Custom Rule",
|
|
4079
4376
|
readOnlyHint: true,
|
|
@@ -4083,6 +4380,8 @@ var testCustomRuleTool = {
|
|
|
4083
4380
|
},
|
|
4084
4381
|
inputSchema: z.object(TestCustomRuleInputShape),
|
|
4085
4382
|
handler: async (input, ctx) => {
|
|
4383
|
+
const inputError = requestUrlRuleInputError(input);
|
|
4384
|
+
if (inputError) return err(inputError);
|
|
4086
4385
|
const result = await ctx.api.testCustomRule({
|
|
4087
4386
|
rule_type: input.rule_type,
|
|
4088
4387
|
config: input.config,
|
|
@@ -4096,22 +4395,22 @@ var testCustomRuleTool = {
|
|
|
4096
4395
|
var UpdateCustomRuleInputShape = {
|
|
4097
4396
|
rule_id: z.string().uuid().describe("Rule UUID to update."),
|
|
4098
4397
|
name: z.string().min(1).max(200).optional().describe(
|
|
4099
|
-
"New display name. For non-LLM rules this
|
|
4398
|
+
"New rule display name. For PERSONAL non-LLM rules this refreshes the auto-registered tag's display name. A same-slug GLOBAL rule keeps its separately managed tag metadata unchanged; use `update_tag_definition` for that tag."
|
|
4100
4399
|
),
|
|
4101
4400
|
tag_slug: z.string().max(100).optional().describe(
|
|
4102
|
-
"New tag slug to assign on match. **MUST NOT collide with a built-in system tag slug** (see `list_tags` where `scope=system`); colliding requests return 422 with code `checking.system_slug_reserved`.
|
|
4401
|
+
"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."
|
|
4103
4402
|
),
|
|
4104
|
-
config:
|
|
4105
|
-
"New rule-type-specific config object. Replaces the stored config wholesale \u2014 resend every key you want to keep, including a combo rule's `match_scope`. For `rule_type='llm'` the keys of `config.tags` are auto-registered as tag definitions; any key that collides with a system slug returns the same 422 contract. " + COMBO_MATCH_SCOPE_DOC
|
|
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
|
|
4106
4405
|
),
|
|
4107
4406
|
target: z.string().max(30).optional().describe(
|
|
4108
|
-
"Where to apply the rule
|
|
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."
|
|
4109
4408
|
),
|
|
4110
4409
|
is_active: z.boolean().optional().describe("Enable/disable the rule.")
|
|
4111
4410
|
};
|
|
4112
4411
|
var updateCustomRuleTool = {
|
|
4113
4412
|
name: "update_custom_rule",
|
|
4114
|
-
description: "Update a custom tag-detection rule. Only supplied fields are sent, but
|
|
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`.",
|
|
4115
4414
|
annotations: {
|
|
4116
4415
|
title: "Update Custom Rule",
|
|
4117
4416
|
readOnlyHint: false,
|
|
@@ -4227,10 +4526,14 @@ var getCustomTaxonomyTool = {
|
|
|
4227
4526
|
return ok(result.value);
|
|
4228
4527
|
}
|
|
4229
4528
|
};
|
|
4230
|
-
var ListCustomTaxonomiesInputShape = {
|
|
4529
|
+
var ListCustomTaxonomiesInputShape = {
|
|
4530
|
+
include_inactive: z.boolean().optional().describe(
|
|
4531
|
+
"Include soft-deleted taxonomies, which are excluded by default. Pass `true` to find one to `restore_custom_taxonomy`, or to resolve a taxonomy id seen on an older scan."
|
|
4532
|
+
)
|
|
4533
|
+
};
|
|
4231
4534
|
var listCustomTaxonomiesTool = {
|
|
4232
4535
|
name: "list_custom_taxonomies",
|
|
4233
|
-
description: "List the calling org's custom classification taxonomies. Returns slim summaries (id, name, slug, version, node_count, is_active).
|
|
4536
|
+
description: "List the calling org's custom classification taxonomies. Returns slim summaries (id, name, slug, version, node_count, is_active). Only active taxonomies are listed unless you pass `include_inactive: true`; soft-deleted ones then appear with `is_active=false`. Fetch the full tree via `get_custom_taxonomy`.",
|
|
4234
4537
|
annotations: {
|
|
4235
4538
|
title: "List Custom Taxonomies",
|
|
4236
4539
|
readOnlyHint: true,
|
|
@@ -4239,8 +4542,10 @@ var listCustomTaxonomiesTool = {
|
|
|
4239
4542
|
openWorldHint: false
|
|
4240
4543
|
},
|
|
4241
4544
|
inputSchema: z.object(ListCustomTaxonomiesInputShape),
|
|
4242
|
-
handler: async (
|
|
4243
|
-
const result = await ctx.api.listCustomTaxonomies(
|
|
4545
|
+
handler: async (input, ctx) => {
|
|
4546
|
+
const result = await ctx.api.listCustomTaxonomies(
|
|
4547
|
+
input.include_inactive !== void 0 ? { include_inactive: input.include_inactive } : {}
|
|
4548
|
+
);
|
|
4244
4549
|
if (result.isErr()) return err(mapApiError(result.error));
|
|
4245
4550
|
return ok({ items: result.value });
|
|
4246
4551
|
}
|
|
@@ -4446,6 +4751,34 @@ var listInvoicesTool = {
|
|
|
4446
4751
|
return ok(result.value);
|
|
4447
4752
|
}
|
|
4448
4753
|
};
|
|
4754
|
+
var MAX_CAMPAIGNS = 500;
|
|
4755
|
+
var AttachPolicySetCampaignsInputShape = {
|
|
4756
|
+
policy_set_id: z.string().uuid().describe("Policy set UUID."),
|
|
4757
|
+
campaign_ids: z.array(z.string().uuid()).min(1).max(MAX_CAMPAIGNS).describe(
|
|
4758
|
+
`Campaign UUIDs to bind, at most ${String(MAX_CAMPAIGNS)} per call. A campaign already on THIS set is a no-op; a campaign on a DIFFERENT set is moved to this one.`
|
|
4759
|
+
)
|
|
4760
|
+
};
|
|
4761
|
+
var attachPolicySetCampaignsTool = {
|
|
4762
|
+
name: "attach_policy_set_campaigns",
|
|
4763
|
+
description: "Bind campaigns to a policy set. This and `detach_policy_set_campaigns` are the only way to change membership \u2014 `update_policy_set` edits the set's name, description and rules, never its campaigns. Membership changes here are incremental: this set's other members are left alone. IMPORTANT: a campaign can belong to only one policy set, so naming a campaign currently on a DIFFERENT set MOVES it here \u2014 it stops being evaluated against its old set's rules, and no warning is returned. Check `list_campaigns` (`policy_set_id`) first when the campaign may already be bound elsewhere. Split lists longer than 500 across calls; verify with `list_policy_set_campaigns`.",
|
|
4764
|
+
annotations: {
|
|
4765
|
+
title: "Attach Policy Set Campaigns",
|
|
4766
|
+
readOnlyHint: false,
|
|
4767
|
+
// Reassigns a campaign away from whatever set it was on, and the
|
|
4768
|
+
// previous binding is not recoverable from this tool's output.
|
|
4769
|
+
destructiveHint: true,
|
|
4770
|
+
idempotentHint: true,
|
|
4771
|
+
openWorldHint: false
|
|
4772
|
+
},
|
|
4773
|
+
inputSchema: z.object(AttachPolicySetCampaignsInputShape),
|
|
4774
|
+
handler: async (input, ctx) => {
|
|
4775
|
+
const result = await ctx.api.attachPolicySetCampaigns(input.policy_set_id, {
|
|
4776
|
+
campaign_ids: [...input.campaign_ids]
|
|
4777
|
+
});
|
|
4778
|
+
if (result.isErr()) return err(mapApiError(result.error));
|
|
4779
|
+
return ok({ attached: input.campaign_ids.length });
|
|
4780
|
+
}
|
|
4781
|
+
};
|
|
4449
4782
|
var TIER1 = z.string().min(1).max(200);
|
|
4450
4783
|
var TIER_OPT = z.string().max(200).nullable().optional();
|
|
4451
4784
|
var IabV3Block = z.object({
|
|
@@ -4590,6 +4923,43 @@ var deletePolicySetTool = {
|
|
|
4590
4923
|
return ok({ deleted: true });
|
|
4591
4924
|
}
|
|
4592
4925
|
};
|
|
4926
|
+
var MAX_CAMPAIGNS2 = 500;
|
|
4927
|
+
var DetachPolicySetCampaignsInputShape = {
|
|
4928
|
+
policy_set_id: z.string().uuid().describe("Policy set UUID."),
|
|
4929
|
+
campaign_ids: z.array(z.string().uuid()).min(1).max(MAX_CAMPAIGNS2).optional().describe(
|
|
4930
|
+
`Campaign UUIDs to unbind, at most ${String(MAX_CAMPAIGNS2)} per call. Omit when using \`detach_all\`.`
|
|
4931
|
+
),
|
|
4932
|
+
detach_all: z.boolean().optional().describe(
|
|
4933
|
+
"Unbind EVERY campaign from this policy set. Check the current membership with `list_policy_set_campaigns` first \u2014 the campaigns stop being evaluated against these rules, so alerts they would have raised simply stop."
|
|
4934
|
+
)
|
|
4935
|
+
};
|
|
4936
|
+
var detachPolicySetCampaignsTool = {
|
|
4937
|
+
name: "detach_policy_set_campaigns",
|
|
4938
|
+
description: "Unbind campaigns from a policy set INCREMENTALLY \u2014 the bindings you don't name survive. Pass `campaign_ids` for specific campaigns or `detach_all: true` to clear the whole membership. This and `attach_policy_set_campaigns` are the only way to change membership; `update_policy_set` edits the set's name, description and rules, never its campaigns. Detaching leaves the policy set and its rules intact, and alerts already raised under it are kept.",
|
|
4939
|
+
annotations: {
|
|
4940
|
+
title: "Detach Policy Set Campaigns",
|
|
4941
|
+
readOnlyHint: false,
|
|
4942
|
+
destructiveHint: true,
|
|
4943
|
+
idempotentHint: true,
|
|
4944
|
+
openWorldHint: false
|
|
4945
|
+
},
|
|
4946
|
+
inputSchema: z.object(DetachPolicySetCampaignsInputShape),
|
|
4947
|
+
handler: async (input, ctx) => {
|
|
4948
|
+
const detachAll = input.detach_all === true;
|
|
4949
|
+
if (input.campaign_ids !== void 0 === detachAll) {
|
|
4950
|
+
return err({
|
|
4951
|
+
kind: "invalid-input",
|
|
4952
|
+
message: "Choose either `campaign_ids` or `detach_all: true` \u2014 not both, and not neither. Passing neither would be a silent no-op."
|
|
4953
|
+
});
|
|
4954
|
+
}
|
|
4955
|
+
const result = await ctx.api.detachPolicySetCampaigns(input.policy_set_id, {
|
|
4956
|
+
detach_all: detachAll,
|
|
4957
|
+
...input.campaign_ids !== void 0 ? { campaign_ids: [...input.campaign_ids] } : {}
|
|
4958
|
+
});
|
|
4959
|
+
if (result.isErr()) return err(mapApiError(result.error));
|
|
4960
|
+
return ok({ detached: detachAll ? "all" : input.campaign_ids?.length ?? 0 });
|
|
4961
|
+
}
|
|
4962
|
+
};
|
|
4593
4963
|
var GetPolicySetInputShape = {
|
|
4594
4964
|
policy_set_id: z.string().uuid().describe("Policy set UUID.")
|
|
4595
4965
|
};
|
|
@@ -4610,6 +4980,33 @@ var getPolicySetTool = {
|
|
|
4610
4980
|
return ok(result.value);
|
|
4611
4981
|
}
|
|
4612
4982
|
};
|
|
4983
|
+
var ListPolicySetCampaignsInputShape = {
|
|
4984
|
+
policy_set_id: z.string().uuid().describe("Policy set UUID."),
|
|
4985
|
+
q: z.string().min(1).max(200).optional().describe("Substring search against campaign name (case-insensitive)."),
|
|
4986
|
+
page: z.number().int().min(1).max(500).default(1).describe("1-indexed page number."),
|
|
4987
|
+
limit: z.number().int().min(1).max(200).default(50).describe("Page size (1-200).")
|
|
4988
|
+
};
|
|
4989
|
+
var listPolicySetCampaignsTool = {
|
|
4990
|
+
name: "list_policy_set_campaigns",
|
|
4991
|
+
description: "List the campaigns bound to a policy set, paginated, each with `id`, `name` and `is_archived`. `get_policy_set` returns only the first page of bindings plus a `campaigns_total`, so use this tool when a set has more campaigns than that page holds, or to search them by name. Read the membership here before `detach_policy_set_campaigns` \u2014 and before `delete_policy_set`, which the API refuses while any active campaign is still bound.",
|
|
4992
|
+
annotations: {
|
|
4993
|
+
title: "List Policy Set Campaigns",
|
|
4994
|
+
readOnlyHint: true,
|
|
4995
|
+
destructiveHint: false,
|
|
4996
|
+
idempotentHint: true,
|
|
4997
|
+
openWorldHint: false
|
|
4998
|
+
},
|
|
4999
|
+
inputSchema: z.object(ListPolicySetCampaignsInputShape),
|
|
5000
|
+
handler: async (input, ctx) => {
|
|
5001
|
+
const result = await ctx.api.listPolicySetCampaigns(input.policy_set_id, {
|
|
5002
|
+
page: input.page,
|
|
5003
|
+
limit: input.limit,
|
|
5004
|
+
...input.q !== void 0 ? { q: input.q } : {}
|
|
5005
|
+
});
|
|
5006
|
+
if (result.isErr()) return err(mapApiError(result.error));
|
|
5007
|
+
return ok(result.value);
|
|
5008
|
+
}
|
|
5009
|
+
};
|
|
4613
5010
|
var ListPolicySetsInputShape = {
|
|
4614
5011
|
page: z.number().int().min(1).max(500).default(1).describe("1-indexed page number."),
|
|
4615
5012
|
limit: z.number().int().min(1).max(200).default(50).describe("Page size."),
|
|
@@ -4895,6 +5292,32 @@ var getScanTool = {
|
|
|
4895
5292
|
return ok(result.value);
|
|
4896
5293
|
}
|
|
4897
5294
|
};
|
|
5295
|
+
var GetScanCreativeHtmlInputShape = {
|
|
5296
|
+
scan_id: z.string().uuid().describe("Scan UUID.")
|
|
5297
|
+
};
|
|
5298
|
+
var getScanCreativeHtmlTool = {
|
|
5299
|
+
name: "get_scan_creative_html",
|
|
5300
|
+
description: "Fetch the generated creative markup for an ad-tag scan as text \u2014 the HTML the ad tag produced, with the scripts, iframes and click-through URLs it embedded. Use it to explain WHY a creative was tagged when a screenshot cannot show it (obfuscated redirectors, hidden trackers, injected handlers). Only ad-tag scans have this artifact; a URL or VAST scan returns not-found. The markup is returned inert as text and is never executed. Markup over 256 KiB is refused \u2014 open the report URL from `get_scan` for those.",
|
|
5301
|
+
annotations: {
|
|
5302
|
+
title: "Get Scan Creative HTML",
|
|
5303
|
+
readOnlyHint: true,
|
|
5304
|
+
destructiveHint: false,
|
|
5305
|
+
idempotentHint: true,
|
|
5306
|
+
openWorldHint: false
|
|
5307
|
+
},
|
|
5308
|
+
inputSchema: z.object(GetScanCreativeHtmlInputShape),
|
|
5309
|
+
handler: async (input, ctx) => {
|
|
5310
|
+
const result = await ctx.api.getScanCreativeHtml(input.scan_id);
|
|
5311
|
+
if (result.isErr()) return err(mapApiError(result.error));
|
|
5312
|
+
const { bytes, contentType } = result.value;
|
|
5313
|
+
return ok({
|
|
5314
|
+
scan_id: input.scan_id,
|
|
5315
|
+
content_type: contentType,
|
|
5316
|
+
byte_size: bytes.byteLength,
|
|
5317
|
+
html: decodeUtf8(bytes)
|
|
5318
|
+
});
|
|
5319
|
+
}
|
|
5320
|
+
};
|
|
4898
5321
|
var GetScanCreativeScreenshotInputShape = {
|
|
4899
5322
|
scan_id: z.string().uuid().describe("Scan UUID \u2014 must be an ad-tag scan (is_ad_tag=true)."),
|
|
4900
5323
|
width: z.number().int().min(50).max(2e3).optional().describe("Optional resize width in pixels.")
|
|
@@ -4916,6 +5339,27 @@ var getScanCreativeScreenshotTool = {
|
|
|
4916
5339
|
return ok(imageBlock(result.value.bytes, result.value.contentType));
|
|
4917
5340
|
}
|
|
4918
5341
|
};
|
|
5342
|
+
var GetScanCreativeVideoInputShape = {
|
|
5343
|
+
scan_id: z.string().uuid().describe("Scan UUID.")
|
|
5344
|
+
};
|
|
5345
|
+
var getScanCreativeVideoTool = {
|
|
5346
|
+
name: "get_scan_creative_video",
|
|
5347
|
+
description: "Fetch the MP4 MediaFile a VAST scan downloaded, as one inline resource block. Only VAST scans have it; others return not-found. Prefer `get_scan_creative_screenshot` for a still frame and `get_scan_vast_xml` for the declared metadata \u2014 both are far cheaper. Reach for the video itself only when the moving image is the evidence. Files over 8 MiB are refused rather than inlined; fall back to `get_scan_creative_screenshot` or the report URL from `get_scan` for those.",
|
|
5348
|
+
annotations: {
|
|
5349
|
+
title: "Get Scan Creative Video",
|
|
5350
|
+
readOnlyHint: true,
|
|
5351
|
+
destructiveHint: false,
|
|
5352
|
+
idempotentHint: true,
|
|
5353
|
+
openWorldHint: false
|
|
5354
|
+
},
|
|
5355
|
+
inputSchema: z.object(GetScanCreativeVideoInputShape),
|
|
5356
|
+
handler: async (input, ctx) => {
|
|
5357
|
+
const result = await ctx.api.getScanCreativeVideo(input.scan_id);
|
|
5358
|
+
if (result.isErr()) return err(mapApiError(result.error));
|
|
5359
|
+
const { bytes, contentType } = result.value;
|
|
5360
|
+
return ok(resourceBlock(bytes, contentType, `/api/v1/scans/${input.scan_id}/creative-video`));
|
|
5361
|
+
}
|
|
5362
|
+
};
|
|
4919
5363
|
var GetScanLandingScreenshotInputShape = {
|
|
4920
5364
|
scan_id: z.string().uuid().describe("Scan UUID (ad-tag scan)."),
|
|
4921
5365
|
landing_ord: z.number().int().min(0).max(50).describe("0-indexed landing slot (see `get_scan(...).landings[*].ord`)."),
|
|
@@ -4967,6 +5411,32 @@ var getScanScreenshotTool = {
|
|
|
4967
5411
|
return ok(imageBlock(result.value.bytes, result.value.contentType));
|
|
4968
5412
|
}
|
|
4969
5413
|
};
|
|
5414
|
+
var GetScanVastXmlInputShape = {
|
|
5415
|
+
scan_id: z.string().uuid().describe("Scan UUID.")
|
|
5416
|
+
};
|
|
5417
|
+
var getScanVastXmlTool = {
|
|
5418
|
+
name: "get_scan_vast_xml",
|
|
5419
|
+
description: "Fetch the VAST document for a VAST scan as text, with wrappers already resolved so the whole chain is visible in one payload. Use it to inspect the tracking pixels, verification vendors, companion ads and click-through targets a video ad declared \u2014 the parsed summary on `get_scan` (`video` block) covers duration, media file and ad system, but not the full element tree. Only VAST scans have this artifact; others return not-found. Documents over 256 KiB are refused.",
|
|
5420
|
+
annotations: {
|
|
5421
|
+
title: "Get Scan VAST XML",
|
|
5422
|
+
readOnlyHint: true,
|
|
5423
|
+
destructiveHint: false,
|
|
5424
|
+
idempotentHint: true,
|
|
5425
|
+
openWorldHint: false
|
|
5426
|
+
},
|
|
5427
|
+
inputSchema: z.object(GetScanVastXmlInputShape),
|
|
5428
|
+
handler: async (input, ctx) => {
|
|
5429
|
+
const result = await ctx.api.getScanVastXml(input.scan_id);
|
|
5430
|
+
if (result.isErr()) return err(mapApiError(result.error));
|
|
5431
|
+
const { bytes, contentType } = result.value;
|
|
5432
|
+
return ok({
|
|
5433
|
+
scan_id: input.scan_id,
|
|
5434
|
+
content_type: contentType,
|
|
5435
|
+
byte_size: bytes.byteLength,
|
|
5436
|
+
xml: decodeUtf8(bytes)
|
|
5437
|
+
});
|
|
5438
|
+
}
|
|
5439
|
+
};
|
|
4970
5440
|
var ListScanChildrenInputShape = {
|
|
4971
5441
|
scan_id: z.string().uuid().describe("The publisher scan's UUID (an ad-discovery scan created with ad_discovery=true)."),
|
|
4972
5442
|
page: z.number().int().min(1).max(500).default(1).describe("1-indexed page number."),
|
|
@@ -4994,7 +5464,7 @@ var listScanChildrenTool = {
|
|
|
4994
5464
|
};
|
|
4995
5465
|
var ListScansInputShape = {
|
|
4996
5466
|
status: z.string().optional().describe(
|
|
4997
|
-
"Comma-separated statuses to filter by. Lifecycle values: pending, running, crawled, checking, checking_async, completed, partial, failed, cancelled."
|
|
5467
|
+
"Comma-separated statuses to filter by. Lifecycle values: pending, running, crawled, checking, checking_async, rechecking, completed, partial, failed, cancelled."
|
|
4998
5468
|
),
|
|
4999
5469
|
country_code: z.string().optional().describe("Comma-separated ISO 3166-1 alpha-2 country codes, e.g. US,DE,JP."),
|
|
5000
5470
|
url: z.string().optional().describe("Substring match against the scanned URL."),
|
|
@@ -5007,7 +5477,13 @@ var ListScansInputShape = {
|
|
|
5007
5477
|
run_id: z.string().uuid().optional().describe("Filter to scans of one run."),
|
|
5008
5478
|
campaign_id: z.string().uuid().optional().describe("Filter to scans of one campaign."),
|
|
5009
5479
|
group_id: z.string().uuid().optional().describe("Filter to scans of one campaign group."),
|
|
5480
|
+
parent_scan_id: z.string().uuid().optional().describe(
|
|
5481
|
+
"Filter to the ad-discovery child scans of one parent scan. Setting this lifts the default 7-day window, so old children stay reachable. `list_scan_children` is the paginated equivalent."
|
|
5482
|
+
),
|
|
5010
5483
|
tag: z.string().optional().describe("Comma-separated tag slugs to filter by."),
|
|
5484
|
+
tag_match: z.enum(["any", "all"]).optional().describe(
|
|
5485
|
+
"How multiple `tag` slugs combine: `any` (default) returns scans carrying at least one, `all` requires every one of them."
|
|
5486
|
+
),
|
|
5011
5487
|
ai_category: z.string().optional().describe(
|
|
5012
5488
|
"Filter by the freeform LLM-generated category prefix on the scan (slash-separated tier path, e.g. 'Gambling/Online Casinos')."
|
|
5013
5489
|
),
|
|
@@ -5054,7 +5530,9 @@ var listScansTool = {
|
|
|
5054
5530
|
...input.run_id !== void 0 ? { run_id: input.run_id } : {},
|
|
5055
5531
|
...input.campaign_id !== void 0 ? { campaign_id: input.campaign_id } : {},
|
|
5056
5532
|
...input.group_id !== void 0 ? { group_id: input.group_id } : {},
|
|
5533
|
+
...input.parent_scan_id !== void 0 ? { parent_scan_id: input.parent_scan_id } : {},
|
|
5057
5534
|
...input.tag !== void 0 ? { tag: input.tag } : {},
|
|
5535
|
+
...input.tag_match !== void 0 ? { tag_match: input.tag_match } : {},
|
|
5058
5536
|
...input.ai_category !== void 0 ? { ai_category: input.ai_category } : {},
|
|
5059
5537
|
...input.iab_v3_category !== void 0 ? { iab_v3_category: input.iab_v3_category } : {},
|
|
5060
5538
|
...input.iab_category !== void 0 ? { iab_category: input.iab_category } : {},
|
|
@@ -5159,11 +5637,14 @@ var listScanTagsTool = {
|
|
|
5159
5637
|
var ListTagsInputShape = {
|
|
5160
5638
|
category: z.string().min(1).max(100).optional().describe(
|
|
5161
5639
|
"Filter to tags in one category slug (e.g. 'security', 'malware', 'compliance'). Use the slug exactly as it appears in `category` of an existing tag."
|
|
5640
|
+
),
|
|
5641
|
+
include_archived: z.boolean().optional().describe(
|
|
5642
|
+
"Include archived tag definitions, which are excluded by default. Archived tags still appear on the scans that carry them, so pass `true` when resolving a slug seen on an older scan."
|
|
5162
5643
|
)
|
|
5163
5644
|
};
|
|
5164
5645
|
var listTagsTool = {
|
|
5165
5646
|
name: "list_tags",
|
|
5166
|
-
description: "List every tag definition the platform knows (system tags + organization custom tags) with category, severity, visibility, and usage counters (scans + rules per tag). Optionally filter by category.",
|
|
5647
|
+
description: "List every tag definition the platform knows (system tags + organization custom tags) with category, severity, visibility, and usage counters (scans + rules per tag). Optionally filter by category, and pass `include_archived` to also see retired definitions.",
|
|
5167
5648
|
annotations: {
|
|
5168
5649
|
title: "List Tags",
|
|
5169
5650
|
readOnlyHint: true,
|
|
@@ -5173,7 +5654,10 @@ var listTagsTool = {
|
|
|
5173
5654
|
},
|
|
5174
5655
|
inputSchema: z.object(ListTagsInputShape),
|
|
5175
5656
|
handler: async (input, ctx) => {
|
|
5176
|
-
const filters =
|
|
5657
|
+
const filters = {
|
|
5658
|
+
...input.category !== void 0 ? { category: input.category } : {},
|
|
5659
|
+
...input.include_archived !== void 0 ? { include_archived: input.include_archived } : {}
|
|
5660
|
+
};
|
|
5177
5661
|
const result = await ctx.api.listTags(filters);
|
|
5178
5662
|
if (result.isErr()) return err(mapApiError(result.error));
|
|
5179
5663
|
return ok({ items: result.value, total: result.value.length });
|
|
@@ -5507,6 +5991,9 @@ function registerAllTools(register) {
|
|
|
5507
5991
|
register(getScanScreenshotTool);
|
|
5508
5992
|
register(getScanCreativeScreenshotTool);
|
|
5509
5993
|
register(getScanLandingScreenshotTool);
|
|
5994
|
+
register(getScanCreativeHtmlTool);
|
|
5995
|
+
register(getScanCreativeVideoTool);
|
|
5996
|
+
register(getScanVastXmlTool);
|
|
5510
5997
|
register(getCampaignTool);
|
|
5511
5998
|
register(listCampaignsTool);
|
|
5512
5999
|
register(archiveCampaignTool);
|
|
@@ -5554,8 +6041,12 @@ function registerAllTools(register) {
|
|
|
5554
6041
|
register(updatePolicySetTool);
|
|
5555
6042
|
register(deletePolicySetTool);
|
|
5556
6043
|
register(requestPolicySetApprovalTool);
|
|
6044
|
+
register(listPolicySetCampaignsTool);
|
|
6045
|
+
register(attachPolicySetCampaignsTool);
|
|
6046
|
+
register(detachPolicySetCampaignsTool);
|
|
5557
6047
|
register(listAlertsTool);
|
|
5558
6048
|
register(updateAlertStatusTool);
|
|
6049
|
+
register(bulkUpdateAlertStatusTool);
|
|
5559
6050
|
register(getAlertStatsTool);
|
|
5560
6051
|
register(listWebhooksTool);
|
|
5561
6052
|
register(getWebhookTool);
|
|
@@ -5659,5 +6150,5 @@ function formatToolError(error) {
|
|
|
5659
6150
|
}
|
|
5660
6151
|
|
|
5661
6152
|
export { BearerToken, SERVER_INSTRUCTIONS, createHttpApiGateway, createPinoLogger, declareEmptyResourcesAndPrompts, newRequestId, wireToolsIntoMcpServer };
|
|
5662
|
-
//# sourceMappingURL=chunk-
|
|
5663
|
-
//# sourceMappingURL=chunk-
|
|
6153
|
+
//# sourceMappingURL=chunk-CUJWJSNJ.js.map
|
|
6154
|
+
//# sourceMappingURL=chunk-CUJWJSNJ.js.map
|