@transcend-io/mcp-server-consent 0.8.4 → 0.9.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.
@@ -1,1287 +0,0 @@
1
- import { EmptySchema, OffsetPaginationSchema, createListResult, createToolResult, defineTool, z } from "@transcend-io/mcp-server-base";
2
- import { AirgapBundleAnalyticsBinInterval, AirgapBundleAnalyticsDimension, AirgapBundleAnalyticsMetric, ConsentManagerAnalyticsDataSource, ConsentManagerMetricBin, ConsentTrackerStatus, ConsentTrackerType, CookieOrderField, DataFlowOrderField, DataFlowScope, OrderDirection, ScopeName, TriageAction } from "@transcend-io/privacy-types";
3
- import { AIRGAP_BUNDLE_AGGREGATE_ANALYTICS, AIRGAP_BUNDLE_TIMESERIES_ANALYTICS, CONSENT_MANAGER_ANALYTICS_DATA, COOKIES, COOKIE_STATS, DATA_FLOWS, EXPERIENCES, FETCH_CONSENT_MANAGER, FETCH_CONSENT_MANAGER_ID, PURPOSES, UPDATE_DATA_FLOWS, UPDATE_OR_CREATE_COOKIES } from "@transcend-io/sdk";
4
- //#region src/resolveAirgapBundleId.ts
5
- const bundleIdCache = /* @__PURE__ */ new WeakMap();
6
- /**
7
- * Lazily resolve the airgap bundle ID from the API key.
8
- * Caches the result per GraphQL client instance so subsequent
9
- * calls return instantly without an extra network request.
10
- */
11
- async function resolveAirgapBundleId(graphql) {
12
- const cached = bundleIdCache.get(graphql);
13
- if (cached) return cached;
14
- const id = (await graphql.makeRequest(FETCH_CONSENT_MANAGER_ID, {})).consentManager.consentManager.id;
15
- bundleIdCache.set(graphql, id);
16
- return id;
17
- }
18
- //#endregion
19
- //#region src/tools/consent_bulk_triage.ts
20
- const BulkTriageItemSchema = z.object({
21
- type: z.nativeEnum(ConsentTrackerType).describe("Item type"),
22
- id: z.string().describe("Item ID (for data flows) or cookie name (for cookies)"),
23
- action: z.nativeEnum(TriageAction).describe("Action to take: APPROVE or JUNK"),
24
- trackingPurposes: z.array(z.string()).optional().describe("Tracking purposes to assign (required when approving)"),
25
- service: z.string().optional().describe("Service name to assign")
26
- });
27
- const BulkTriageSchema = z.object({ items: z.array(BulkTriageItemSchema).min(1).describe("Items to triage") });
28
- function createConsentBulkTriageTool(clients) {
29
- return defineTool({
30
- name: "consent_bulk_triage",
31
- description: "Bulk triage action: approve or junk multiple cookies and data flows in a single call. For cookies, APPROVE sets status=LIVE; JUNK sets isJunk=true. For data flows, same behavior. Optionally assign tracking purposes and service when approving.",
32
- category: "Consent Management",
33
- readOnly: false,
34
- annotations: {
35
- readOnlyHint: false,
36
- destructiveHint: true,
37
- idempotentHint: false
38
- },
39
- zodSchema: BulkTriageSchema,
40
- handler: async ({ items }) => {
41
- const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
42
- const cookieItems = items.filter((i) => i.type === "cookie");
43
- const dfItems = items.filter((i) => i.type === "data_flow");
44
- const results = {
45
- cookies: [],
46
- dataFlows: []
47
- };
48
- if (cookieItems.length > 0) {
49
- const cookieInputs = cookieItems.map((item) => ({
50
- name: item.id,
51
- ...item.action === "APPROVE" ? {
52
- status: ConsentTrackerStatus.Live,
53
- isJunk: false
54
- } : {
55
- status: ConsentTrackerStatus.Live,
56
- isJunk: true
57
- },
58
- ...item.trackingPurposes ? { trackingPurposes: item.trackingPurposes } : {},
59
- ...item.service ? { service: item.service } : {}
60
- }));
61
- await clients.graphql.makeRequest(UPDATE_OR_CREATE_COOKIES, {
62
- airgapBundleId,
63
- cookies: cookieInputs
64
- });
65
- results.cookies = cookieInputs.map((c) => ({
66
- name: c.name,
67
- action: c.isJunk ? "JUNKED" : "APPROVED",
68
- status: c.status || "LIVE"
69
- }));
70
- }
71
- if (dfItems.length > 0) {
72
- const dfInputs = dfItems.map((item) => ({
73
- id: item.id,
74
- ...item.action === "APPROVE" ? {
75
- status: ConsentTrackerStatus.Live,
76
- isJunk: false
77
- } : {
78
- status: ConsentTrackerStatus.Live,
79
- isJunk: true
80
- },
81
- ...item.trackingPurposes ? { purposeIds: item.trackingPurposes } : {},
82
- ...item.service ? { service: item.service } : {}
83
- }));
84
- results.dataFlows = (await clients.graphql.makeRequest(UPDATE_DATA_FLOWS, {
85
- airgapBundleId,
86
- dataFlows: dfInputs
87
- })).updateDataFlows.dataFlows.map((df) => ({
88
- id: df.id,
89
- action: df.isJunk ? "JUNKED" : "APPROVED",
90
- status: df.status
91
- }));
92
- }
93
- return createToolResult(true, {
94
- totalProcessed: cookieItems.length + dfItems.length,
95
- ...results
96
- });
97
- }
98
- });
99
- }
100
- //#endregion
101
- //#region src/analyticsDateRange.ts
102
- /**
103
- * Resolve a date range from explicit ISO timestamps or a lookback window.
104
- */
105
- function resolveAnalyticsDateRange(args) {
106
- const endDate = args.end ? new Date(args.end) : /* @__PURE__ */ new Date();
107
- const lookbackDays = args.days ?? 7;
108
- const startDate = args.start ? new Date(args.start) : /* @__PURE__ */ new Date(endDate.getTime() - lookbackDays * 24 * 60 * 60 * 1e3);
109
- if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) throw new Error("Invalid start or end date");
110
- if (startDate > endDate) throw new Error("Start date must be before end date");
111
- return {
112
- startEpoch: Math.floor(startDate.getTime() / 1e3),
113
- endEpoch: Math.floor(endDate.getTime() / 1e3),
114
- startIso: startDate.toISOString(),
115
- endIso: endDate.toISOString()
116
- };
117
- }
118
- //#endregion
119
- //#region src/normalizeAnalyticsMetric.ts
120
- const VALID_METRICS = new Set(Object.values(AirgapBundleAnalyticsMetric));
121
- /** Common agent/API guesses mapped to GraphQL AnalyticsEvent values */
122
- const ANALYTICS_METRIC_ALIASES = {
123
- PAGE_VIEW: AirgapBundleAnalyticsMetric.PageViews,
124
- CONSENT_SESSION: AirgapBundleAnalyticsMetric.SiteSessions,
125
- CONSENT_SESSIONS: AirgapBundleAnalyticsMetric.SiteSessions
126
- };
127
- /**
128
- * Normalize metric input, accepting common aliases (e.g. PAGE_VIEW → PAGE_VIEWS).
129
- */
130
- function normalizeAnalyticsMetric(metric) {
131
- const upper = metric.toUpperCase();
132
- if (VALID_METRICS.has(upper)) return upper;
133
- return ANALYTICS_METRIC_ALIASES[upper] ?? upper;
134
- }
135
- const airgapBundleAnalyticsMetricSchema = z.preprocess((value) => typeof value === "string" ? normalizeAnalyticsMetric(value) : value, z.nativeEnum(AirgapBundleAnalyticsMetric));
136
- //#endregion
137
- //#region src/tools/consent_get_aggregate_analytics.ts
138
- const GetAggregateAnalyticsSchema = z.object({
139
- metric: airgapBundleAnalyticsMetricSchema.describe("Analytics metric to query. CONSENT_CHANGED for opt-in/out counts; SITE_SESSIONS or PAGE_VIEWS for traffic totals."),
140
- start: z.string().optional().describe("Start datetime (ISO 8601). Defaults to `days` lookback from end."),
141
- end: z.string().optional().describe("End datetime (ISO 8601). Defaults to now."),
142
- days: z.coerce.number().min(1).max(365).optional().describe("Lookback window in days when start is omitted (default: 7)."),
143
- include_dimensions: z.array(z.nativeEnum(AirgapBundleAnalyticsDimension)).optional().describe("Dimension breakdowns (e.g. NEW_VALUE, REGIME, PURPOSE). Recommended for CONSENT_CHANGED.")
144
- });
145
- function createConsentGetAggregateAnalyticsTool(clients) {
146
- return defineTool({
147
- name: "consent_get_aggregate_analytics",
148
- description: "Query aggregate consent analytics via airgapBundleAggregateAnalytics. Use CONSENT_CHANGED with NEW_VALUE/REGIME/PURPOSE for opt-in/out counts; SITE_SESSIONS or PAGE_VIEWS for total traffic. Requires ViewConsentManager API key scope.",
149
- category: "Consent Management",
150
- readOnly: true,
151
- annotations: {
152
- readOnlyHint: true,
153
- destructiveHint: false,
154
- idempotentHint: true
155
- },
156
- zodSchema: GetAggregateAnalyticsSchema,
157
- handler: async ({ metric, start, end, days, include_dimensions }) => {
158
- const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
159
- const range = resolveAnalyticsDateRange({
160
- start,
161
- end,
162
- days
163
- });
164
- const items = (await clients.graphql.makeRequest(AIRGAP_BUNDLE_AGGREGATE_ANALYTICS, {
165
- id: airgapBundleId,
166
- input: {
167
- metric,
168
- start: range.startEpoch,
169
- end: range.endEpoch,
170
- ...include_dimensions?.length ? { includeDimensions: include_dimensions } : {}
171
- }
172
- })).airgapBundleAggregateAnalytics.items;
173
- return createToolResult(true, {
174
- airgapBundleId,
175
- metric,
176
- period: {
177
- start: range.startIso,
178
- end: range.endIso,
179
- startEpoch: range.startEpoch,
180
- endEpoch: range.endEpoch
181
- },
182
- items,
183
- totalRows: items.length
184
- });
185
- }
186
- });
187
- }
188
- //#endregion
189
- //#region src/tools/consent_get_analytics_data.ts
190
- const GetAnalyticsDataSchema = z.object({
191
- data_source: z.nativeEnum(ConsentManagerAnalyticsDataSource).describe("analyticsData source: PRIVACY_SIGNAL_TIMESERIES (DNT/GPC), CONSENT_CHANGES_TIMESERIES (opt-in/out), or CONSENT_SESSIONS_BY_REGIME."),
192
- start: z.string().optional().describe("Start datetime (ISO 8601). Defaults to `days` lookback from end."),
193
- end: z.string().optional().describe("End datetime (ISO 8601). Defaults to now."),
194
- days: z.coerce.number().min(1).max(365).optional().describe("Lookback window in days when start is omitted (default: 7)."),
195
- bin: z.nativeEnum(ConsentManagerMetricBin).optional().default(ConsentManagerMetricBin.Daily).describe("Time bin size for analyticsData (1h or 1d, default: 1d).")
196
- });
197
- function createConsentGetAnalyticsDataTool(clients) {
198
- return defineTool({
199
- name: "consent_get_analytics_data",
200
- description: "Query consent metrics via the analyticsData GraphQL query. Returns timeseries for privacy signals (DNT/GPC), consent changes (opt-in/out), or sessions by regime. Requires ViewConsentManager API key scope.",
201
- category: "Consent Management",
202
- readOnly: true,
203
- annotations: {
204
- readOnlyHint: true,
205
- destructiveHint: false,
206
- idempotentHint: true
207
- },
208
- zodSchema: GetAnalyticsDataSchema,
209
- handler: async ({ data_source, start, end, days, bin }) => {
210
- const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
211
- const range = resolveAnalyticsDateRange({
212
- start,
213
- end,
214
- days
215
- });
216
- const series = (await clients.graphql.makeRequest(CONSENT_MANAGER_ANALYTICS_DATA, { input: {
217
- dataSource: data_source,
218
- startDate: range.startIso,
219
- endDate: range.endIso,
220
- forceRefetch: true,
221
- airgapBundleId,
222
- binInterval: bin,
223
- smoothTimeseries: false
224
- } })).analyticsData.series;
225
- return createToolResult(true, {
226
- airgapBundleId,
227
- dataSource: data_source,
228
- binInterval: bin,
229
- period: {
230
- start: range.startIso,
231
- end: range.endIso
232
- },
233
- series
234
- });
235
- }
236
- });
237
- }
238
- //#endregion
239
- //#region src/getDataFlowCount.ts
240
- /**
241
- * Fetch `dataFlows.totalCount` without paging nodes.
242
- *
243
- * Uses `first: 1` so the payload stays small. The list API hides CSP rows
244
- * (same as the Consent Manager table), so these counts match what users see.
245
- */
246
- async function getDataFlowCount(graphql, airgapBundleId, filterBy) {
247
- return (await graphql.makeRequest(DATA_FLOWS, {
248
- input: { airgapBundleId },
249
- first: 1,
250
- offset: 0,
251
- filterBy
252
- })).dataFlows.totalCount;
253
- }
254
- //#endregion
255
- //#region src/tools/consent_get_inventory_stats.ts
256
- const GetInventoryStatsSchema = z.object({});
257
- function createConsentGetInventoryStatsTool(clients) {
258
- return defineTool({
259
- name: "consent_get_inventory_stats",
260
- description: "Get cookie and data-flow inventory triage counts: live (approved), needs review, and junk. Counts match the Consent Manager tables and the default consent_list_cookies / consent_list_data_flows filters (CSP data flows are omitted, same as the UI). This is inventory status, not consent analytics — use consent_get_aggregate_analytics or consent_get_timeseries_analytics for opt-in/out and signal metrics.",
261
- category: "Consent Management",
262
- readOnly: true,
263
- annotations: {
264
- readOnlyHint: true,
265
- destructiveHint: false,
266
- idempotentHint: true
267
- },
268
- zodSchema: GetInventoryStatsSchema,
269
- handler: async () => {
270
- const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
271
- const [cookieData, needReviewCount, liveCount, junkCount] = await Promise.all([
272
- clients.graphql.makeRequest(COOKIE_STATS, { input: { airgapBundleId } }),
273
- getDataFlowCount(clients.graphql, airgapBundleId, { status: ConsentTrackerStatus.NeedsReview }),
274
- getDataFlowCount(clients.graphql, airgapBundleId, {
275
- status: ConsentTrackerStatus.Live,
276
- isJunk: false
277
- }),
278
- getDataFlowCount(clients.graphql, airgapBundleId, {
279
- status: ConsentTrackerStatus.Live,
280
- isJunk: true
281
- })
282
- ]);
283
- return createToolResult(true, {
284
- cookies: cookieData.cookieStats,
285
- dataFlows: {
286
- liveCount,
287
- needReviewCount,
288
- junkCount
289
- }
290
- });
291
- }
292
- });
293
- }
294
- //#endregion
295
- //#region src/tools/consent_get_preferences.ts
296
- const GetPreferencesSchema = z.object({
297
- identifier: z.string().describe("User identifier (e.g., email, user ID)"),
298
- partition: z.string().optional().describe("Partition/organization context (optional)")
299
- });
300
- function createConsentGetPreferencesTool(clients) {
301
- const { rest } = clients;
302
- return defineTool({
303
- name: "consent_get_preferences",
304
- description: "Get consent preferences for a specific user/identifier",
305
- category: "Consent Management",
306
- readOnly: true,
307
- annotations: {
308
- readOnlyHint: true,
309
- destructiveHint: false,
310
- idempotentHint: true
311
- },
312
- requireSombra: true,
313
- zodSchema: GetPreferencesSchema,
314
- handler: async ({ identifier, partition }) => {
315
- const result = await rest.getConsentPreferences(identifier, partition);
316
- if (!result) return createToolResult(true, {
317
- found: false,
318
- message: "No consent preferences found for this identifier"
319
- });
320
- return createToolResult(true, {
321
- found: true,
322
- preferences: result
323
- });
324
- }
325
- });
326
- }
327
- //#endregion
328
- //#region src/tools/consent_get_timeseries_analytics.ts
329
- const GetTimeseriesAnalyticsSchema = z.object({
330
- metric: airgapBundleAnalyticsMetricSchema.describe("Analytics metric to query. PAGE_VIEWS for daily page-view volume; SITE_SESSIONS for sessions; SIGNAL_DETECTED for GPC/DNT signal counts over time."),
331
- start: z.string().optional().describe("Start datetime (ISO 8601). Defaults to `days` lookback from end."),
332
- end: z.string().optional().describe("End datetime (ISO 8601). Defaults to now."),
333
- days: z.coerce.number().min(1).max(365).optional().describe("Lookback window in days when start is omitted (default: 7)."),
334
- bin_interval: z.nativeEnum(AirgapBundleAnalyticsBinInterval).optional().default(AirgapBundleAnalyticsBinInterval.Hourly).describe("Time bin size: 1m, 1h, or 1d (default: 1h).")
335
- });
336
- function createConsentGetTimeseriesAnalyticsTool(clients) {
337
- return defineTool({
338
- name: "consent_get_timeseries_analytics",
339
- description: "Query timeseries consent analytics via airgapBundleTimeseriesAnalytics. Use PAGE_VIEWS or SITE_SESSIONS for traffic volume; SIGNAL_DETECTED for privacy signal counts. Requires ViewConsentManager API key scope.",
340
- category: "Consent Management",
341
- readOnly: true,
342
- annotations: {
343
- readOnlyHint: true,
344
- destructiveHint: false,
345
- idempotentHint: true
346
- },
347
- zodSchema: GetTimeseriesAnalyticsSchema,
348
- handler: async ({ metric, start, end, days, bin_interval }) => {
349
- const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
350
- const range = resolveAnalyticsDateRange({
351
- start,
352
- end,
353
- days
354
- });
355
- const items = (await clients.graphql.makeRequest(AIRGAP_BUNDLE_TIMESERIES_ANALYTICS, {
356
- id: airgapBundleId,
357
- input: {
358
- metric,
359
- start: range.startEpoch,
360
- end: range.endEpoch,
361
- binInterval: bin_interval
362
- }
363
- })).airgapBundleTimeseriesAnalytics.items;
364
- return createToolResult(true, {
365
- airgapBundleId,
366
- metric,
367
- binInterval: bin_interval,
368
- period: {
369
- start: range.startIso,
370
- end: range.endIso,
371
- startEpoch: range.startEpoch,
372
- endEpoch: range.endEpoch
373
- },
374
- items,
375
- totalRows: items.length
376
- });
377
- }
378
- });
379
- }
380
- //#endregion
381
- //#region src/tools/consent_list_airgap_bundles.ts
382
- const ListAirgapBundlesSchema = EmptySchema;
383
- function createConsentListAirgapBundlesTool(clients) {
384
- return defineTool({
385
- name: "consent_list_airgap_bundles",
386
- description: "Get the consent manager (airgap bundle) configured for your organization. Returns the bundle ID, URLs, configuration, and domains.",
387
- category: "Consent Management",
388
- readOnly: true,
389
- annotations: {
390
- readOnlyHint: true,
391
- destructiveHint: false,
392
- idempotentHint: true
393
- },
394
- zodSchema: ListAirgapBundlesSchema,
395
- handler: async (_args) => {
396
- return createToolResult(true, (await clients.graphql.makeRequest(FETCH_CONSENT_MANAGER, {})).consentManager.consentManager);
397
- }
398
- });
399
- }
400
- //#endregion
401
- //#region src/tools/consent_list_cookies.ts
402
- const ListCookiesSchema = OffsetPaginationSchema.extend({
403
- status: z.nativeEnum(ConsentTrackerStatus).describe("Filter by status: NEEDS_REVIEW (triage) or LIVE (approved)"),
404
- isJunk: z.boolean().optional().describe("Filter by junk status"),
405
- showZeroActivity: z.boolean().optional().describe("Include items with zero activity. Omit (default) so the NEEDS_REVIEW total matches consent_get_inventory_stats cookies.needReviewCount; set true for the full triage backlog including never-active cookies."),
406
- text: z.string().optional().describe("Search text filter"),
407
- service: z.string().optional().describe("Filter by service name"),
408
- minOccurrences: z.number().min(0).optional().describe("Only return cookies with at least this many occurrences (traffic)"),
409
- orderField: z.nativeEnum(CookieOrderField).optional().describe("Field to sort by (e.g. occurrences to rank by traffic)"),
410
- orderDirection: z.nativeEnum(OrderDirection).optional().describe("Sort direction: ASC or DESC")
411
- });
412
- function createConsentListCookiesTool(clients) {
413
- return defineTool({
414
- name: "consent_list_cookies",
415
- description: "List cookies in your consent manager. Requires a status filter: NEEDS_REVIEW for triage backlog, LIVE for approved cookies. Returns name, service, tracking purposes, activity (occurrences), junk status, and more. Sort by occurrences (orderField=occurrences, orderDirection=DESC) to surface top-traffic cookies, and use minOccurrences to filter low-traffic noise.",
416
- category: "Consent Management",
417
- readOnly: true,
418
- annotations: {
419
- readOnlyHint: true,
420
- destructiveHint: false,
421
- idempotentHint: true
422
- },
423
- zodSchema: ListCookiesSchema,
424
- handler: async ({ first, offset, status, isJunk, showZeroActivity, text, service, minOccurrences, orderField, orderDirection }) => {
425
- const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
426
- const { nodes, totalCount } = (await clients.graphql.makeRequest(COOKIES, {
427
- input: { airgapBundleId },
428
- first,
429
- offset,
430
- filterBy: {
431
- status,
432
- ...isJunk !== void 0 ? { isJunk } : {},
433
- ...showZeroActivity !== void 0 ? { showZeroActivity } : {},
434
- ...text ? { text } : {},
435
- ...service ? { service } : {},
436
- ...minOccurrences !== void 0 ? { minOccurrences } : {}
437
- },
438
- ...orderField && orderDirection ? { orderBy: [{
439
- field: orderField,
440
- direction: orderDirection
441
- }] } : {}
442
- })).cookies;
443
- return createListResult(nodes, {
444
- totalCount,
445
- hasNextPage: offset + nodes.length < totalCount
446
- });
447
- }
448
- });
449
- }
450
- //#endregion
451
- //#region src/tools/consent_list_data_flows.ts
452
- const ListDataFlowsSchema = OffsetPaginationSchema.extend({
453
- status: z.nativeEnum(ConsentTrackerStatus).describe("Filter by status: NEEDS_REVIEW (triage) or LIVE (approved)"),
454
- isJunk: z.boolean().optional().describe("Filter by junk status"),
455
- showZeroActivity: z.boolean().optional().describe("Include items with zero activity. Omit (default) so the NEEDS_REVIEW total matches consent_get_inventory_stats dataFlows.needReviewCount (the Consent Manager table). Set true for the full triage backlog including never-active flows."),
456
- text: z.string().optional().describe("Search text filter"),
457
- service: z.string().optional().describe("Filter by service name"),
458
- unmappedOnly: z.boolean().optional().describe("Return only unmapped/orphaned flows with no associated service (catalog integration). Useful with status=LIVE to find approved flows that are not mapped to a service."),
459
- type: z.nativeEnum(DataFlowScope).optional().describe("Filter by data flow scope type (e.g. HOST, PATH, REGEX, CSP)"),
460
- minOccurrences: z.number().min(0).optional().describe("Only return flows with at least this many occurrences (traffic)"),
461
- orderField: z.nativeEnum(DataFlowOrderField).optional().describe("Field to sort by"),
462
- orderDirection: z.nativeEnum(OrderDirection).optional().describe("Sort direction: ASC or DESC")
463
- });
464
- function createConsentListDataFlowsTool(clients) {
465
- return defineTool({
466
- name: "consent_list_data_flows",
467
- description: "List data flows (network requests) in your consent manager. Requires a status filter: NEEDS_REVIEW for triage backlog, LIVE for approved flows. Returns value (URL/host), service, tracking purposes, activity (occurrences), and more. Use unmappedOnly to find approved flows with no service, type to filter by scope (e.g. CSP), and minOccurrences to focus on high-traffic flows.",
468
- category: "Consent Management",
469
- readOnly: true,
470
- annotations: {
471
- readOnlyHint: true,
472
- destructiveHint: false,
473
- idempotentHint: true
474
- },
475
- zodSchema: ListDataFlowsSchema,
476
- handler: async ({ first, offset, status, isJunk, showZeroActivity, text, service, unmappedOnly, type, minOccurrences, orderField, orderDirection }) => {
477
- const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
478
- const { nodes, totalCount } = (await clients.graphql.makeRequest(DATA_FLOWS, {
479
- input: { airgapBundleId },
480
- first,
481
- offset,
482
- filterBy: {
483
- status,
484
- ...isJunk !== void 0 ? { isJunk } : {},
485
- ...showZeroActivity !== void 0 ? { showZeroActivity } : {},
486
- ...text ? { text } : {},
487
- ...unmappedOnly ? { service: "" } : service ? { service } : {},
488
- ...type ? { type } : {},
489
- ...minOccurrences !== void 0 ? { minOccurrences } : {}
490
- },
491
- ...orderField && orderDirection ? { orderBy: [{
492
- field: orderField,
493
- direction: orderDirection
494
- }] } : {}
495
- })).dataFlows;
496
- return createListResult(nodes, {
497
- totalCount,
498
- hasNextPage: offset + nodes.length < totalCount
499
- });
500
- }
501
- });
502
- }
503
- //#endregion
504
- //#region src/tools/consent_list_purposes.ts
505
- const ListPurposesSchema = z.object({ limit: z.coerce.number().min(1).max(100).optional().default(50).describe("Maximum number of purposes to return (1-100, default 50).") });
506
- function createConsentListPurposesTool(clients) {
507
- return defineTool({
508
- name: "consent_list_purposes",
509
- description: "List all tracking purposes configured for consent management (max ~100 results).",
510
- category: "Consent Management",
511
- readOnly: true,
512
- annotations: {
513
- readOnlyHint: true,
514
- destructiveHint: false,
515
- idempotentHint: true
516
- },
517
- zodSchema: ListPurposesSchema,
518
- handler: async ({ limit }) => {
519
- const { nodes, totalCount } = (await clients.graphql.makeRequest(PURPOSES, { first: Math.min(limit, 100) })).purposes;
520
- return createListResult(nodes, {
521
- totalCount,
522
- hasNextPage: nodes.length < totalCount
523
- });
524
- }
525
- });
526
- }
527
- //#endregion
528
- //#region src/tools/consent_list_regimes.ts
529
- const ListRegimesSchema = z.object({
530
- limit: z.coerce.number().min(1).max(100).optional().default(50).describe("Maximum number of regimes to return per page (1-100, default 50)."),
531
- offset: z.coerce.number().min(0).optional().default(0).describe("Number of results to skip for pagination (default 0).")
532
- });
533
- function createConsentListRegimesTool(clients) {
534
- return defineTool({
535
- name: "consent_list_regimes",
536
- description: "List all consent experiences (regional regimes) configured for your organization. Returns experience name, regions, purposes, opted-out purposes, and view state.",
537
- category: "Consent Management",
538
- readOnly: true,
539
- annotations: {
540
- readOnlyHint: true,
541
- destructiveHint: false,
542
- idempotentHint: true
543
- },
544
- zodSchema: ListRegimesSchema,
545
- handler: async ({ limit, offset }) => {
546
- const { totalCount, nodes } = (await clients.graphql.makeRequest(EXPERIENCES, {
547
- first: limit,
548
- offset
549
- })).experiences;
550
- return createListResult(nodes, {
551
- totalCount,
552
- hasNextPage: offset + nodes.length < totalCount
553
- });
554
- }
555
- });
556
- }
557
- //#endregion
558
- //#region src/tools/consent_set_preferences.ts
559
- const PurposeConsentSchema = z.object({
560
- purpose: z.string().describe("Purpose slug"),
561
- enabled: z.boolean().describe("Whether consent is granted")
562
- });
563
- const SetPreferencesSchema = z.object({
564
- identifier: z.string().optional().describe("User identifier"),
565
- partition: z.string().describe("Partition/organization context"),
566
- purposes: z.array(PurposeConsentSchema).describe("Array of purpose consent settings"),
567
- confirmed: z.boolean().optional().describe("Whether consent was explicitly confirmed")
568
- });
569
- function createConsentSetPreferencesTool(clients) {
570
- const { rest } = clients;
571
- return defineTool({
572
- name: "consent_set_preferences",
573
- description: "Set consent preferences for a user (client-side sync)",
574
- category: "Consent Management",
575
- readOnly: false,
576
- annotations: {
577
- readOnlyHint: false,
578
- destructiveHint: false,
579
- idempotentHint: true
580
- },
581
- requireSombra: true,
582
- zodSchema: SetPreferencesSchema,
583
- handler: async ({ partition, identifier, purposes, confirmed }) => {
584
- return createToolResult(true, {
585
- ...await rest.syncConsent({
586
- partition,
587
- identifier,
588
- purposes: purposes.map((p) => ({
589
- purpose: p.purpose,
590
- enabled: p.enabled
591
- })),
592
- confirmed
593
- }),
594
- message: "Consent preferences synced successfully"
595
- });
596
- }
597
- });
598
- }
599
- //#endregion
600
- //#region src/tools/consent_update_cookies.ts
601
- const UpdateCookieItemSchema = z.object({
602
- name: z.string().describe("Cookie name (used as the identifier for upsert)"),
603
- trackingPurposes: z.array(z.string()).optional().describe("Tracking purpose slugs (e.g., \"Advertising\", \"Analytics\")"),
604
- description: z.string().optional().describe("Cookie description"),
605
- service: z.string().optional().describe("Service/integration name"),
606
- isJunk: z.boolean().optional().describe("Mark as junk"),
607
- status: z.nativeEnum(ConsentTrackerStatus).optional().describe("Set status to LIVE (approve) or NEEDS_REVIEW")
608
- });
609
- const UpdateCookiesSchema = z.object({ cookies: z.array(UpdateCookieItemSchema).min(1).describe("Cookies to update") });
610
- function createConsentUpdateCookiesTool(clients) {
611
- return defineTool({
612
- name: "consent_update_cookies",
613
- description: "Update one or more cookies. Use to approve (status=LIVE), junk (isJunk=true), assign tracking purposes, or set a service. The cookie \"name\" field is the identifier for upsert — existing cookies with matching names will be updated.",
614
- category: "Consent Management",
615
- readOnly: false,
616
- annotations: {
617
- readOnlyHint: false,
618
- destructiveHint: true,
619
- idempotentHint: true
620
- },
621
- zodSchema: UpdateCookiesSchema,
622
- handler: async ({ cookies }) => {
623
- const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
624
- const cookieInputs = cookies.map((c) => ({
625
- name: c.name,
626
- ...c.trackingPurposes ? { trackingPurposes: c.trackingPurposes } : {},
627
- ...c.description !== void 0 ? { description: c.description } : {},
628
- ...c.service !== void 0 ? { service: c.service } : {},
629
- ...c.isJunk !== void 0 ? { isJunk: c.isJunk } : {},
630
- ...c.status !== void 0 ? { status: c.status } : {}
631
- }));
632
- await clients.graphql.makeRequest(UPDATE_OR_CREATE_COOKIES, {
633
- airgapBundleId,
634
- cookies: cookieInputs
635
- });
636
- return createToolResult(true, {
637
- updated: cookieInputs.length,
638
- cookies: cookieInputs.map((c) => ({
639
- name: c.name,
640
- status: c.status,
641
- isJunk: c.isJunk,
642
- trackingPurposes: c.trackingPurposes,
643
- service: c.service
644
- }))
645
- });
646
- }
647
- });
648
- }
649
- //#endregion
650
- //#region src/tools/consent_update_data_flows.ts
651
- const UpdateDataFlowItemSchema = z.object({
652
- id: z.string().describe("Data flow ID"),
653
- trackingPurposes: z.array(z.string()).optional().describe("Tracking purpose slugs"),
654
- description: z.string().optional().describe("Data flow description"),
655
- service: z.string().optional().describe("Service/integration name"),
656
- isJunk: z.boolean().optional().describe("Mark as junk"),
657
- status: z.nativeEnum(ConsentTrackerStatus).optional().describe("Set status to LIVE (approve) or NEEDS_REVIEW")
658
- });
659
- const UpdateDataFlowsSchema = z.object({ dataFlows: z.array(UpdateDataFlowItemSchema).min(1).describe("Data flows to update") });
660
- function createConsentUpdateDataFlowsTool(clients) {
661
- return defineTool({
662
- name: "consent_update_data_flows",
663
- description: "Update one or more data flows. Use to approve (status=LIVE), junk (isJunk=true), assign tracking purposes, or set a service.",
664
- category: "Consent Management",
665
- readOnly: false,
666
- annotations: {
667
- readOnlyHint: false,
668
- destructiveHint: true,
669
- idempotentHint: true
670
- },
671
- zodSchema: UpdateDataFlowsSchema,
672
- handler: async ({ dataFlows }) => {
673
- const airgapBundleId = await resolveAirgapBundleId(clients.graphql);
674
- const dfInputs = dataFlows.map((df) => ({
675
- id: df.id,
676
- ...df.trackingPurposes ? { purposeIds: df.trackingPurposes } : {},
677
- ...df.description !== void 0 ? { description: df.description } : {},
678
- ...df.service !== void 0 ? { service: df.service } : {},
679
- ...df.isJunk !== void 0 ? { isJunk: df.isJunk } : {},
680
- ...df.status !== void 0 ? { status: df.status } : {}
681
- }));
682
- const data = await clients.graphql.makeRequest(UPDATE_DATA_FLOWS, {
683
- airgapBundleId,
684
- dataFlows: dfInputs
685
- });
686
- return createToolResult(true, {
687
- updated: data.updateDataFlows.dataFlows.length,
688
- dataFlows: data.updateDataFlows.dataFlows.map((df) => ({
689
- id: df.id,
690
- value: df.value,
691
- status: df.status,
692
- isJunk: df.isJunk,
693
- purposes: df.purposes.map((p) => p.name),
694
- service: df.service?.title
695
- }))
696
- });
697
- }
698
- });
699
- }
700
- //#endregion
701
- //#region src/tools/index.ts
702
- function getConsentTools(clients) {
703
- return [
704
- createConsentGetPreferencesTool(clients),
705
- createConsentSetPreferencesTool(clients),
706
- createConsentListPurposesTool(clients),
707
- createConsentListDataFlowsTool(clients),
708
- createConsentListCookiesTool(clients),
709
- createConsentListAirgapBundlesTool(clients),
710
- createConsentListRegimesTool(clients),
711
- createConsentGetInventoryStatsTool(clients),
712
- createConsentGetAggregateAnalyticsTool(clients),
713
- createConsentGetTimeseriesAnalyticsTool(clients),
714
- createConsentGetAnalyticsDataTool(clients),
715
- createConsentUpdateCookiesTool(clients),
716
- createConsentUpdateDataFlowsTool(clients),
717
- createConsentBulkTriageTool(clients)
718
- ];
719
- }
720
- //#endregion
721
- //#region src/prompts/consent_inspect_site.ts
722
- const consentInspectSitePrompt = {
723
- name: "consent-inspect-site",
724
- description: "Live site investigation methodology for consent triage using browser DevTools. Covers regime overrides, consent verification, performance entries, HTML search, ad infrastructure checks, and airgap classification queries.",
725
- arguments: [
726
- {
727
- name: "site_url",
728
- description: "The site to investigate (e.g. \"https://example.com\")",
729
- required: true
730
- },
731
- {
732
- name: "tracker_domains",
733
- description: "Comma-separated tracker domains to look for (e.g. \"doubleclick.net,google-analytics.com\")",
734
- required: true
735
- },
736
- {
737
- name: "regime",
738
- description: "The most permissive regime name for URL override (e.g. \"us\"). Choose the regime with fewest opted-out purposes so trackers fire.",
739
- required: false
740
- }
741
- ],
742
- handler: (args) => {
743
- const siteUrl = args.site_url || "(not specified)";
744
- const trackerDomains = args.tracker_domains || "(not specified)";
745
- const regime = args.regime || "us";
746
- const domainList = trackerDomains.split(",").map((d) => d.trim()).filter(Boolean);
747
- const domainArrayLiteral = JSON.stringify(domainList);
748
- return [{
749
- role: "user",
750
- content: {
751
- type: "text",
752
- text: `Investigate how these trackers load on ${siteUrl}: ${trackerDomains}. Use regime "${regime}" for debug overrides.`
753
- }
754
- }, {
755
- role: "assistant",
756
- content: {
757
- type: "text",
758
- text: `## Live Site Investigation
759
-
760
- ### Important: Platform vs Client Sites
761
-
762
- The bundle name (e.g. "acme-platform") may be a platform provider, not the actual
763
- site with trackers. If the main domain is a corporate page without ad trackers, find a
764
- real client site from links on the homepage and use that instead.
765
-
766
- ### Step 1: Navigate with Debug Overrides
767
-
768
- Load the page with hash parameters to control consent behavior:
769
-
770
- \`\`\`
771
- ${siteUrl}/#tcm-regime=${regime}&tcm-prompt=Hidden&log=*
772
- \`\`\`
773
-
774
- | Parameter | Purpose |
775
- |-----------|---------|
776
- | \`tcm-regime=${regime}\` | Force the most permissive privacy regime |
777
- | \`tcm-prompt=Hidden\` | Suppress the consent banner |
778
- | \`log=*\` | Enable verbose airgap debug logging |
779
-
780
- When \`docs_list\` / \`docs_fetch\` are available, fetch the debugging article for full detail; otherwise open:
781
- https://docs.transcend.io/docs/articles/consent-management/reference/debugging-and-testing
782
-
783
- ### Step 2: Verify Consent State
784
-
785
- \`\`\`javascript
786
- (() => {
787
- if (!window.airgap) return 'airgap not loaded';
788
- return JSON.stringify({
789
- regimes: airgap.getRegimes(),
790
- purposes: airgap.getConsent().purposes,
791
- regimePurposes: airgap.getRegimePurposes(),
792
- }, null, 2);
793
- })()
794
- \`\`\`
795
-
796
- All purposes should be \`true\` or \`"Auto"\`. If not, opt in manually:
797
-
798
- \`\`\`javascript
799
- (() => {
800
- airgap.optIn(Object.fromEntries(
801
- airgap.getRegimePurposes().map(p => [p, true])
802
- ));
803
- return JSON.stringify(airgap.getConsent().purposes);
804
- })()
805
- \`\`\`
806
-
807
- ### Step 3: Check Performance Entries for Tracker Domains
808
-
809
- \`\`\`javascript
810
- (() => {
811
- const domains = ${domainArrayLiteral};
812
- const entries = performance.getEntriesByType('resource');
813
- const results = {};
814
- for (const d of domains) {
815
- results[d] = entries.filter(e => e.name.includes(d)).map(e => ({
816
- url: e.name,
817
- initiator: e.initiatorType,
818
- duration: Math.round(e.duration),
819
- size: e.transferSize,
820
- }));
821
- }
822
- return JSON.stringify(results, null, 2);
823
- })()
824
- \`\`\`
825
-
826
- ### Step 4: Search Page HTML
827
-
828
- \`\`\`javascript
829
- (() => {
830
- const terms = ${domainArrayLiteral};
831
- const html = document.documentElement.outerHTML;
832
- const results = {};
833
- for (const term of terms) {
834
- const matches = [];
835
- let i = 0;
836
- while ((i = html.indexOf(term, i)) !== -1) {
837
- matches.push(html.substring(Math.max(0, i - 100), Math.min(html.length, i + 100)));
838
- i += term.length;
839
- if (matches.length > 3) break;
840
- }
841
- results[term] = { count: matches.length, samples: matches };
842
- }
843
- return JSON.stringify(results, null, 2);
844
- })()
845
- \`\`\`
846
-
847
- ### Step 5: Identify Ad Infrastructure
848
-
849
- \`\`\`javascript
850
- (() => {
851
- const scripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src);
852
- // Non-exhaustive list of common ad tech scripts; look for any third-party ad scripts beyond these
853
- const adScripts = scripts.filter(s =>
854
- s.includes('prebid') || s.includes('gpt.js') || s.includes('googletag') ||
855
- s.includes('taboola') || s.includes('criteo') || s.includes('amazon-adsystem') ||
856
- s.includes('adsbygoogle') || s.includes('doubleclick')
857
- );
858
- const adDivs = Array.from(document.querySelectorAll(
859
- '[data-prebid], [data-ad], [data-ad-slot], [data-ad-unit], [id*="ad-slot"], [id*="ad-unit"], [class*="ad-container"]'
860
- ));
861
- const adSlots = adDivs.map(d => ({
862
- tag: d.tagName, id: d.id, class: d.className?.substring(0, 60),
863
- dataSizes: d.getAttribute('data-sizes'),
864
- dataPrebid: d.getAttribute('data-prebid'),
865
- dataTargeting: d.getAttribute('data-targeting'),
866
- }));
867
- const iframes = Array.from(document.querySelectorAll('iframe'));
868
- const adIframes = iframes.filter(f => f.title?.includes('ad') || f.id?.includes('ad'));
869
- return JSON.stringify({
870
- adScripts,
871
- adSlotCount: adSlots.length,
872
- adSlotSamples: adSlots.slice(0, 5),
873
- adIframes: adIframes.map(f => ({
874
- id: f.id, src: f.src?.substring(0, 150), title: f.title,
875
- })),
876
- }, null, 2);
877
- })()
878
- \`\`\`
879
-
880
- ### Step 6: Check Inline Initialization Scripts
881
-
882
- \`\`\`javascript
883
- (() => {
884
- const scripts = Array.from(document.querySelectorAll('script:not([src])'));
885
- const adInline = scripts.filter(s =>
886
- s.textContent.includes('prebid') || s.textContent.includes('googletag') ||
887
- s.textContent.includes('adsbygoogle') || s.textContent.includes('criteo') ||
888
- s.textContent.includes('taboola')
889
- );
890
- return JSON.stringify(adInline.map(s => ({
891
- parent: s.parentElement?.tagName,
892
- preview: s.textContent.substring(0, 500),
893
- })), null, 2);
894
- })()
895
- \`\`\`
896
-
897
- ### Step 7: Check Window Globals and Ad Config
898
-
899
- \`\`\`javascript
900
- (() => {
901
- const knownAdGlobals = ['pbjs', 'googletag', '__tcfapi', '__gpp', '__cmp',
902
- 'adsbygoogle', '_taboola', 'criteo_q', 'apstag'];
903
- const adGlobals = Object.keys(window).filter(k =>
904
- knownAdGlobals.some(g => k.toLowerCase().includes(g.toLowerCase()))
905
- );
906
- const configs = {};
907
- for (const g of adGlobals) {
908
- try {
909
- const val = window[g];
910
- if (val && typeof val === 'object') {
911
- configs[g] = JSON.stringify(val).substring(0, 500);
912
- }
913
- } catch {}
914
- }
915
- return JSON.stringify({ adGlobals, configs }, null, 2);
916
- })()
917
- \`\`\`
918
-
919
- ### Step 8: Check Airgap Classification Per Tracker
920
-
921
- \`\`\`javascript
922
- (async () => {
923
- if (!window.airgap) return 'airgap not loaded';
924
- const domains = ${domainArrayLiteral};
925
- const results = {};
926
- for (const d of domains) {
927
- try {
928
- const purposes = await airgap.getPurposes('https://' + d + '/');
929
- const allowed = await airgap.isAllowed('https://' + d + '/');
930
- results[d] = { purposes, allowed };
931
- } catch (e) { results[d] = { error: e.message }; }
932
- }
933
- return JSON.stringify(results, null, 2);
934
- })()
935
- \`\`\`
936
-
937
- ### Step 9: Read Console Logs
938
-
939
- Read the browser console output. The \`log=*\` override makes airgap emit detailed
940
- allow/block decisions for every request, including purpose lookups. Search these logs
941
- for each tracker domain to see how airgap classifies and handles it.
942
-
943
- ## Useful Console Commands Reference
944
-
945
- | Command | Purpose |
946
- |---------|---------|
947
- | \`airgap.getConsent().purposes\` | Current consent state per purpose |
948
- | \`airgap.getRegimes()\` | Active regime(s) for this session |
949
- | \`airgap.getRegimePurposes()\` | Purposes regulated under current regime |
950
- | \`await airgap.getPurposes('{url}')\` | What purposes a URL is classified under |
951
- | \`await airgap.isAllowed('{url}')\` | Whether a URL is currently allowed |
952
- | \`await airgap.isCookieAllowed({name:'{name}'})\` | Whether a cookie is allowed |
953
- | \`await airgap.getCookiePurposes({name:'{name}'})\` | Cookie's assigned purposes |
954
- | \`airgap.export().requests\` | Quarantined requests |
955
- | \`airgap.export().cookies\` | Quarantined cookies |
956
- | \`airgap.version\` | Current airgap version |
957
-
958
- ## Output Format
959
-
960
- For each tracker return:
961
-
962
- \`\`\`json
963
- {
964
- "domain": "<domain>",
965
- "found_on_page": true,
966
- "loading_method": "direct_script|tag_manager|iframe|dynamic|not_found",
967
- "loaded_by": "<what script or mechanism loads it>",
968
- "in_main_document": true,
969
- "airgap_purposes": ["Advertising"],
970
- "airgap_allowed": true,
971
- "ad_infrastructure": "<detected ad chain, e.g. Prebid -> GPT>",
972
- "related_config": "<relevant config values>",
973
- "notes": "<additional observations>"
974
- }
975
- \`\`\`
976
-
977
- Also return a site summary:
978
-
979
- \`\`\`json
980
- {
981
- "site_investigated": "<actual URL used>",
982
- "ad_stack": "<detected stack, e.g. Prebid -> Google Publisher Tags>",
983
- "consent_manager": "Transcend CMP",
984
- "total_ad_slots": "<count>",
985
- "total_scripts": "<count>",
986
- "total_iframes": "<count>"
987
- }
988
- \`\`\``
989
- }
990
- }];
991
- }
992
- };
993
- //#endregion
994
- //#region src/prompts/consent_research_tracker.ts
995
- const consentResearchTrackerPrompt = {
996
- name: "consent-research-tracker",
997
- description: "Research methodology for classifying cookies and data flows. Covers company identification, privacy policy lookup, CMP database checks, and structured evidence gathering for consent purpose assignment.",
998
- arguments: [
999
- {
1000
- name: "domain",
1001
- description: "The tracker domain or cookie name to research (e.g. \"doubleclick.net\", \"_ga\")",
1002
- required: true
1003
- },
1004
- {
1005
- name: "type",
1006
- description: "Whether this is a \"cookie\" or \"data_flow\" (default: \"data_flow\")",
1007
- required: false
1008
- },
1009
- {
1010
- name: "available_purposes",
1011
- description: "Comma-separated list of the customer's configured purposes (e.g. \"Essential,Functional,Analytics,Advertising,SaleOfInfo\"). Only recommend purposes from this list.",
1012
- required: false
1013
- }
1014
- ],
1015
- handler: (args) => {
1016
- const domain = args.domain || "(not specified)";
1017
- return [{
1018
- role: "user",
1019
- content: {
1020
- type: "text",
1021
- text: `Research the ${args.type || "data_flow"} "${domain}" to determine its consent classification. Available purposes: ${args.available_purposes || "(fetch from consent_list_purposes)"}`
1022
- }
1023
- }, {
1024
- role: "assistant",
1025
- content: {
1026
- type: "text",
1027
- text: `## Research Methodology
1028
-
1029
- For each tracker or cookie, follow these steps in order:
1030
-
1031
- ### Step 1: Company Identification
1032
-
1033
- Search the root domain (strip subdomains for broader matches) to find the operating company.
1034
- Check for recent acquisitions or rebrands — ad tech companies frequently change ownership.
1035
-
1036
- ### Step 2: First-Party Privacy Docs
1037
-
1038
- Find and read the company's privacy policy and/or cookie policy. Look for:
1039
- - How they classify their own tracking
1040
- - What data they collect
1041
- - Stated purposes for data processing
1042
- - Data retention periods
1043
-
1044
- ### Step 3: Service Description
1045
-
1046
- Understand the business model:
1047
- - Ad tech (DSP, SSP, ad exchange, header bidding)?
1048
- - Analytics (pageview counters, session recording, A/B testing)?
1049
- - CMP (consent management platform)?
1050
- - CDN / performance (content delivery, image optimization)?
1051
- - Functional (chat, support, preferences, authentication)?
1052
- - Data broker (selling/sharing data with third parties)?
1053
-
1054
- ### Step 4: CMP Database Lookups
1055
-
1056
- Search these databases for existing classifications:
1057
-
1058
- | Database | URL | Use For |
1059
- |----------|-----|---------|
1060
- | CookieDatabase.org | https://cookiedatabase.org/ | Cookie name lookup |
1061
- | better.fyi trackers | https://better.fyi/trackers/ | Domain-to-company lookup |
1062
- | Ghostery TrackerDB | https://www.ghostery.com/trackerdb | Tracker classification |
1063
- | Cookiepedia | https://cookiepedia.co.uk/ | Cookie purpose database |
1064
- | BuiltWith | https://builtwith.com/ | Site technology stack |
1065
- | urlscan.io | https://urlscan.io/ | Domain/infrastructure analysis |
1066
-
1067
- ### Step 5: Third-Party Cookie Policies
1068
-
1069
- Find other companies' published cookie policies that classify this same tracker/service.
1070
- Multiple independent classifications strengthen confidence.
1071
-
1072
- ### Step 6: Essential vs Non-Essential Determination
1073
-
1074
- Based on all evidence:
1075
- - Would the site break without this tracker? (Essential)
1076
- - Is it required for core functionality like auth, security, or the CMP itself? (Essential)
1077
- - Does it enhance features without being required? (Functional)
1078
- - Does it measure usage or behavior? (Analytics)
1079
- - Does it serve, target, or retarget ads? (Advertising)
1080
- - Is data sold or shared with third parties for their own use? (SaleOfInfo)
1081
-
1082
- Items can have multiple purposes (e.g. ["Advertising", "Analytics"] for an ad platform
1083
- that also tracks impressions).
1084
-
1085
- IMPORTANT: Only recommend purposes from the customer's configured list. If research
1086
- suggests a purpose that doesn't exist for this customer, flag it and suggest the closest
1087
- available match.
1088
-
1089
- ## Junk Indicators
1090
-
1091
- Mark as JUNK (not a real tracker to classify) if:
1092
- - From a browser extension (Grammarly, LastPass, ad blockers injecting scripts)
1093
- - Malware or unwanted injection not placed by the site operator
1094
- - A development/testing artifact (localhost, staging URLs)
1095
- - A subdomain variant of an already-approved regex rule
1096
-
1097
- ## Confidence Levels
1098
-
1099
- - **High**: First-party docs confirm, OR multiple CMPs agree, OR well-known tracker
1100
- - **Medium**: Some evidence but no definitive first-party documentation
1101
- - **Low**: No docs found, best-guess only — flag for manual review
1102
-
1103
- ## Output Format
1104
-
1105
- Return a structured finding for each item:
1106
-
1107
- \`\`\`json
1108
- {
1109
- "domain": "<domain or cookie name>",
1110
- "company_name": "<identified company>",
1111
- "company_description": "<what the company does, 1-2 sentences>",
1112
- "service_url": "<company homepage>",
1113
- "specific_product": "<what product/feature this domain serves>",
1114
- "recommended_purposes": ["Advertising"],
1115
- "confidence": "High",
1116
- "is_junk": false,
1117
- "evidence_summary": "<2-3 sentence summary with key facts>",
1118
- "sources": ["<url1>", "<url2>"],
1119
- "suggested_description": "<one-line description to save as Transcend note>",
1120
- "first_party_privacy_url": "<URL of their privacy/cookie policy if found>",
1121
- "other_cmps_classify_as": "<what other CMPs say>"
1122
- }
1123
- \`\`\``
1124
- }
1125
- }];
1126
- }
1127
- };
1128
- //#endregion
1129
- //#region src/prompts/consent_triage.ts
1130
- const consentTriagePrompt = {
1131
- name: "consent-triage",
1132
- description: "Systematically triage cookies and data flows discovered by Transcend consent telemetry. Walks through setup, batch fetching, research, review, and classification push.",
1133
- arguments: [{
1134
- name: "triage_type",
1135
- description: "What to triage: \"cookies\", \"data_flows\", or \"both\" (default: \"both\")",
1136
- required: false
1137
- }, {
1138
- name: "batch_size",
1139
- description: "Number of items per batch (default: 10)",
1140
- required: false
1141
- }],
1142
- handler: (args) => {
1143
- const triageType = args.triage_type || "both";
1144
- const batchSize = args.batch_size || "10";
1145
- return [{
1146
- role: "user",
1147
- content: {
1148
- type: "text",
1149
- text: `Triage ${triageType === "both" ? "cookies and data flows" : triageType} in batches of ${batchSize}, sorted by highest traffic first.`
1150
- }
1151
- }, {
1152
- role: "assistant",
1153
- content: {
1154
- type: "text",
1155
- text: `I'll walk through the consent triage workflow. Here's how it works:
1156
-
1157
- ## Phase 1: Setup
1158
-
1159
- Gather the customer's consent configuration by calling these tools in parallel:
1160
-
1161
- 1. \`consent_list_airgap_bundles\` — get the consent manager info (bundle ID is auto-resolved)
1162
- 2. \`consent_get_inventory_stats\` — backlog overview
1163
- 3. \`consent_list_purposes\` — the customer's configured tracking purposes
1164
- 4. \`consent_list_regimes\` — consent experiences with regions, purposes, and opt-out defaults
1165
-
1166
- CRITICAL: Each customer configures their own purposes. Do NOT assume defaults exist. Only use purposes returned by \`consent_list_purposes\` for classification.
1167
-
1168
- From the regimes data, determine:
1169
- - Which purposes can be opted out of per experience
1170
- - Which purposes default to opted-out
1171
- - The most permissive regime (fewest opted-out purposes) — needed for live site investigation
1172
-
1173
- Present the customer's setup:
1174
-
1175
- | Purpose | Slug | Used in Regimes |
1176
- |---------|------|-----------------|
1177
- | (from API) | (from API) | (cross-ref with regimes) |
1178
-
1179
- Present triage stats from \`consent_get_inventory_stats\` (cookie and data-flow counts match the Consent Manager tables; CSP data flows are omitted like the UI):
1180
-
1181
- | Metric | Cookies | Data Flows |
1182
- |--------|---------|------------|
1183
- | Needs Review | cookies.needReviewCount | dataFlows.needReviewCount |
1184
- | Live (Approved) | cookies.liveCount | dataFlows.liveCount |
1185
- | Junk | cookies.junkCount | dataFlows.junkCount |
1186
-
1187
- ## Phase 2: Fetch Batch
1188
-
1189
- Fetch the next batch of items needing review, sorted by highest traffic:
1190
-
1191
- ${[triageType === "cookies" || triageType === "both" ? "- `consent_list_cookies { status: \"NEEDS_REVIEW\", first: " + batchSize + ", order_field: \"occurrences\", order_direction: \"DESC\" }`" : "", triageType === "data_flows" || triageType === "both" ? "- `consent_list_data_flows { status: \"NEEDS_REVIEW\", first: " + batchSize + ", order_field: \"occurrences\", order_direction: \"DESC\" }`" : ""].filter(Boolean).join("\n")}
1192
-
1193
- Present in this table format:
1194
-
1195
- | # | Name/Domain | Type | Service | Auto-Purposes | Occurrences | Sites | First Seen |
1196
- |---|-------------|------|---------|---------------|-------------|-------|------------|
1197
-
1198
- ## Phase 3: Research
1199
-
1200
- For each item in the batch, research its purpose using web search and CMP databases.
1201
- Use the \`consent-research-tracker\` prompt for detailed research methodology.
1202
- If browser/DevTools access is available, use the \`consent-inspect-site\` prompt for live site investigation.
1203
-
1204
- Split items into parallel research groups of 3–5 items each for efficiency.
1205
-
1206
- ## Phase 4: Present Findings
1207
-
1208
- For each researched item, present:
1209
-
1210
- ### {name/domain}
1211
- | Field | Value |
1212
- |-------|-------|
1213
- | Type | Cookie / Data Flow (HOST/REGEX) |
1214
- | Domain | \`example.com\` |
1215
- | Service | Service Name (or "Unknown") |
1216
- | Current Purposes | What Transcend auto-classified (if any) |
1217
- | Recommended Purpose | Research-based recommendation |
1218
- | Confidence | High / Medium / Low |
1219
- | How Loaded | Direct script / Tag manager / iframe / Dynamic |
1220
- | Occurrences | N |
1221
- | Evidence | Brief summary + source URLs |
1222
- | Recommended Action | APPROVE with purposes / JUNK / NEEDS MANUAL REVIEW |
1223
- | Suggested Note | Description to save to Transcend |
1224
-
1225
- Then show a summary action table:
1226
-
1227
- | # | Name/Domain | Action | Purposes | Service | Note |
1228
- |---|-------------|--------|----------|---------|------|
1229
-
1230
- Ask the user to confirm, modify, or reject each recommendation before proceeding.
1231
-
1232
- ## Phase 5: Push Classifications
1233
-
1234
- For confirmed items, update Transcend:
1235
-
1236
- - Individual updates with notes: \`consent_update_data_flows\` / \`consent_update_cookies\` with id, tracking_purposes, description, service, status: "LIVE"
1237
- - Bulk approve/junk: \`consent_bulk_triage\` with items array containing type, id, action, tracking_purposes
1238
- - Mark junk items with action "JUNK" (no purposes needed)
1239
-
1240
- After pushing, report what was updated and show the remaining triage count.
1241
-
1242
- ## Phase 6: Loop
1243
-
1244
- Ask the user if they want to continue with the next batch. Repeat from Phase 2.
1245
-
1246
- ## Key References
1247
-
1248
- When \`docs_list\` / \`docs_fetch\` are available (e.g. the unified \`@transcend-io/mcp\` server), prefer those for full markdown. Otherwise open the docs URLs directly:
1249
-
1250
- - Triage guide: https://docs.transcend.io/docs/articles/consent-management/configuration/triage-cookies-and-dataflows-guide
1251
- - Data flows & cookies: https://docs.transcend.io/docs/articles/consent-management/concepts/data-flows-and-cookies
1252
- - Tracking purposes: https://docs.transcend.io/docs/articles/consent-management/concepts/tracking-purposes
1253
- - Regional experiences: https://docs.transcend.io/docs/articles/consent-management/configuration/regional-experiences
1254
- - Telemetry overview: https://docs.transcend.io/docs/articles/consent-management/configuration/telemetry-overview`
1255
- }
1256
- }];
1257
- }
1258
- };
1259
- //#endregion
1260
- //#region src/prompts/index.ts
1261
- /**
1262
- * Returns consent workflow prompt templates for MCP prompts/list and prompts/get.
1263
- *
1264
- * @param _clients - Unused; accepted so createMCPServer can pass the same factory shape as getTools
1265
- */
1266
- function getConsentPrompts(_clients) {
1267
- return [
1268
- consentTriagePrompt,
1269
- consentResearchTrackerPrompt,
1270
- consentInspectSitePrompt
1271
- ];
1272
- }
1273
- //#endregion
1274
- //#region src/scopes.ts
1275
- /** OAuth scopes required for Consent MCP tools (offline_access added by base). */
1276
- const CONSENT_OAUTH_SCOPES = [
1277
- ScopeName.ViewConsentManager,
1278
- ScopeName.ViewAssignedConsentManager,
1279
- ScopeName.ManageConsentManager,
1280
- ScopeName.ManageAssignedConsentManager,
1281
- ScopeName.ViewDataFlow,
1282
- ScopeName.ManageDataFlow
1283
- ];
1284
- //#endregion
1285
- export { resolveAirgapBundleId as C, BulkTriageSchema as S, GetInventoryStatsSchema as _, UpdateDataFlowsSchema as a, resolveAnalyticsDateRange as b, PurposeConsentSchema as c, ListPurposesSchema as d, ListDataFlowsSchema as f, GetPreferencesSchema as g, GetTimeseriesAnalyticsSchema as h, UpdateDataFlowItemSchema as i, SetPreferencesSchema as l, ListAirgapBundlesSchema as m, getConsentPrompts as n, UpdateCookieItemSchema as o, ListCookiesSchema as p, getConsentTools as r, UpdateCookiesSchema as s, CONSENT_OAUTH_SCOPES as t, ListRegimesSchema as u, GetAnalyticsDataSchema as v, BulkTriageItemSchema as x, GetAggregateAnalyticsSchema as y };
1286
-
1287
- //# sourceMappingURL=scopes-UQvBqSH8.mjs.map