@revclamp/google-ads-transparency-mcp-actor 0.0.0-stage → 1.0.1
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/.actor/actor.json +17 -0
- package/.actor/input_schema.json +76 -0
- package/.well-known/glama.json +6 -0
- package/LICENSE +21 -0
- package/README.md +39 -2
- package/actor_config.json +29 -0
- package/assets/icon-180.png +0 -0
- package/assets/icon-32.png +0 -0
- package/assets/icon.svg +26 -0
- package/assets/playground.css +468 -0
- package/assets/playground.js +795 -0
- package/glama.json +6 -0
- package/index.mjs +222 -0
- package/package.json +38 -4
- package/server.json +36 -0
- package/src/apify_actor.mjs +1714 -0
- package/src/egress_guard.mjs +69 -0
- package/src/mcp_server.mjs +1100 -0
- package/src/rpc_client.mjs +3632 -0
- package/src/web_preview_server.mjs +420 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const ALLOWED_ASSET_HOSTS = new Set([
|
|
5
|
+
'adstransparency.google.com',
|
|
6
|
+
'displayads-formats.googleusercontent.com',
|
|
7
|
+
'tpc.googlesyndication.com',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function isSafeExternalAssetUrl(rawUrl) {
|
|
11
|
+
if (typeof rawUrl !== 'string' || !rawUrl.trim()) return false;
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(rawUrl);
|
|
14
|
+
if (parsed.protocol !== 'https:') return false;
|
|
15
|
+
return ALLOWED_ASSET_HOSTS.has(parsed.hostname.toLowerCase());
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function clearChildren(node) {
|
|
22
|
+
while (node.firstChild) {
|
|
23
|
+
node.removeChild(node.firstChild);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createEl(tag, className, text) {
|
|
28
|
+
const el = document.createElement(tag);
|
|
29
|
+
if (className) el.className = className;
|
|
30
|
+
if (text !== undefined && text !== null) {
|
|
31
|
+
el.textContent = String(text);
|
|
32
|
+
}
|
|
33
|
+
return el;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const state = {
|
|
37
|
+
tools: [],
|
|
38
|
+
selectedToolName: 'search_google_ads_transparency',
|
|
39
|
+
lastNextPageToken: null,
|
|
40
|
+
loadPreviewsOptIn: false,
|
|
41
|
+
lastResultPayload: null,
|
|
42
|
+
rpcRequestId: 1,
|
|
43
|
+
activeRequestToken: 0,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const TOOL_UI_META = {
|
|
47
|
+
search_google_ads_transparency: {
|
|
48
|
+
title: 'Search Google Ads Transparency Creatives',
|
|
49
|
+
fields: {
|
|
50
|
+
query: {
|
|
51
|
+
title: 'Brand Name or Domain',
|
|
52
|
+
placeholder: 'e.g. nike.com or Stripe',
|
|
53
|
+
hint: 'Enter a brand name or domain to match verified Google Ads Transparency advertiser entities.',
|
|
54
|
+
},
|
|
55
|
+
advertiserId: {
|
|
56
|
+
title: 'Advertiser ID (Optional)',
|
|
57
|
+
placeholder: 'e.g. AR16735076323512287233',
|
|
58
|
+
hint: 'Optional Google Ads Transparency Advertiser ID (AR followed by 10 to 25 digits).',
|
|
59
|
+
},
|
|
60
|
+
countryCode: {
|
|
61
|
+
title: 'Country Filter (Optional)',
|
|
62
|
+
placeholder: 'e.g. US',
|
|
63
|
+
hint: 'Optional 2-letter ISO country code filter (leave blank for all regions).',
|
|
64
|
+
},
|
|
65
|
+
formatFilter: {
|
|
66
|
+
title: 'Ad Format',
|
|
67
|
+
hint: 'Filter returned ad creatives by format (ALL, TEXT, IMAGE, or VIDEO).',
|
|
68
|
+
},
|
|
69
|
+
includeSubsidiaries: {
|
|
70
|
+
title: 'Include Regional Subsidiaries',
|
|
71
|
+
hint: 'Include up to 3 verified regional legal entities for the same brand.',
|
|
72
|
+
},
|
|
73
|
+
includeCreativeDetails: {
|
|
74
|
+
title: 'Include Creative Region & Legal Details',
|
|
75
|
+
hint: 'Fetch regional presence and legal entity details for up to 5 returned creatives.',
|
|
76
|
+
},
|
|
77
|
+
maxEntities: {
|
|
78
|
+
title: 'Max Advertiser Entities',
|
|
79
|
+
placeholder: '1 to 3',
|
|
80
|
+
hint: 'Maximum number of verified advertiser entities to query (1 to 3).',
|
|
81
|
+
},
|
|
82
|
+
limit: {
|
|
83
|
+
title: 'Max Creatives per Call',
|
|
84
|
+
placeholder: '1 to 25',
|
|
85
|
+
hint: 'Maximum number of creatives returned in a single call (1 to 25).',
|
|
86
|
+
},
|
|
87
|
+
pageToken: {
|
|
88
|
+
title: 'Next-Page Cursor Token (Optional)',
|
|
89
|
+
placeholder: 'Paste continuation token from a previous run',
|
|
90
|
+
hint: 'Optional continuation token returned by a previous call to load the next page.',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
resolve_google_ads_advertiser: {
|
|
95
|
+
title: 'Resolve Google Ads Advertiser Entities',
|
|
96
|
+
fields: {
|
|
97
|
+
query: {
|
|
98
|
+
title: 'Brand Name or Domain',
|
|
99
|
+
placeholder: 'e.g. nike.com or Stripe',
|
|
100
|
+
hint: 'Brand name or domain to resolve into ranked verified advertiser legal entities.',
|
|
101
|
+
},
|
|
102
|
+
countryCode: {
|
|
103
|
+
title: 'Preferred Country (Optional)',
|
|
104
|
+
placeholder: 'e.g. US',
|
|
105
|
+
hint: 'Optional 2-letter ISO country code preference when ranking regional entities.',
|
|
106
|
+
},
|
|
107
|
+
includeSubsidiaries: {
|
|
108
|
+
title: 'Include Regional Subsidiaries',
|
|
109
|
+
hint: 'Return up to 3 verified regional legal entities matching the brand.',
|
|
110
|
+
},
|
|
111
|
+
maxEntities: {
|
|
112
|
+
title: 'Max Advertiser Entities',
|
|
113
|
+
placeholder: '1 to 3',
|
|
114
|
+
hint: 'Maximum number of verified regional legal entities to select (1 to 3).',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
get_google_ads_creative_detail: {
|
|
119
|
+
title: 'Get Google Ads Creative Detail',
|
|
120
|
+
fields: {
|
|
121
|
+
advertiserId: {
|
|
122
|
+
title: 'Advertiser ID',
|
|
123
|
+
placeholder: 'e.g. AR16735076323512287233',
|
|
124
|
+
hint: 'Required Google Ads Transparency Advertiser ID (starts with AR followed by 10 to 25 digits).',
|
|
125
|
+
},
|
|
126
|
+
creativeId: {
|
|
127
|
+
title: 'Creative ID',
|
|
128
|
+
placeholder: 'e.g. CR02320987639562076161',
|
|
129
|
+
hint: 'Required Google Ads Transparency Creative ID (starts with CR followed by 10 to 25 digits).',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const els = {
|
|
136
|
+
toolSelect: document.getElementById('tool-select'),
|
|
137
|
+
dynamicFields: document.getElementById('dynamic-schema-fields'),
|
|
138
|
+
loadPreviewsCheckbox: document.getElementById('toggle-load-previews'),
|
|
139
|
+
runBtn: document.getElementById('btn-run-tool'),
|
|
140
|
+
nextPageBtn: document.getElementById('btn-load-next-page'),
|
|
141
|
+
copyJsonBtn: document.getElementById('btn-copy-json'),
|
|
142
|
+
downloadJsonBtn: document.getElementById('btn-download-json'),
|
|
143
|
+
statusBanner: document.getElementById('status-banner'),
|
|
144
|
+
billingModeValue: document.getElementById('telemetry-billing-mode'),
|
|
145
|
+
extractedCountValue: document.getElementById('telemetry-extracted-count'),
|
|
146
|
+
paginationStatusValue: document.getElementById('telemetry-pagination-status'),
|
|
147
|
+
envelopeBytesValue: document.getElementById('telemetry-envelope-bytes'),
|
|
148
|
+
cardsContainer: document.getElementById('results-cards-container'),
|
|
149
|
+
rawJsonBox: document.getElementById('raw-json-output'),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
function setExportButtonsEnabled(enabled) {
|
|
153
|
+
const isEnabled = Boolean(enabled);
|
|
154
|
+
if (els.copyJsonBtn) {
|
|
155
|
+
els.copyJsonBtn.disabled = !isEnabled;
|
|
156
|
+
}
|
|
157
|
+
if (els.downloadJsonBtn) {
|
|
158
|
+
els.downloadJsonBtn.disabled = !isEnabled;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function clearExportableResultState(clearCards = false, resetTelemetry = false) {
|
|
163
|
+
state.lastResultPayload = null;
|
|
164
|
+
setExportButtonsEnabled(false);
|
|
165
|
+
if (clearCards && els.cardsContainer) {
|
|
166
|
+
clearChildren(els.cardsContainer);
|
|
167
|
+
}
|
|
168
|
+
if (resetTelemetry) {
|
|
169
|
+
if (els.billingModeValue) els.billingModeValue.textContent = 'Local mode - not billed';
|
|
170
|
+
if (els.extractedCountValue) els.extractedCountValue.textContent = '0';
|
|
171
|
+
if (els.paginationStatusValue) els.paginationStatusValue.textContent = 'IDLE';
|
|
172
|
+
if (els.envelopeBytesValue) els.envelopeBytesValue.textContent = '0 B';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function humanizeRpcErrorMessage(rpcError, toolName = state.selectedToolName) {
|
|
177
|
+
const code = Number(rpcError?.code);
|
|
178
|
+
const rawMsg = String(rpcError?.message || 'Request failed');
|
|
179
|
+
const countryLabel =
|
|
180
|
+
TOOL_UI_META[toolName]?.fields?.countryCode?.title?.replace(/\s*\(Optional\)$/i, '') ||
|
|
181
|
+
'Country Filter';
|
|
182
|
+
const cleanMsg = rawMsg
|
|
183
|
+
.replace(/^Invalid params:\s*/i, '')
|
|
184
|
+
.replace(/\bcountryCode\b/g, countryLabel)
|
|
185
|
+
.replace(/\badvertiserId\b/g, 'Advertiser ID')
|
|
186
|
+
.replace(/\bcreativeId\b/g, 'Creative ID')
|
|
187
|
+
.replace(/\bformatFilter\b/g, 'Ad Format')
|
|
188
|
+
.replace(/\bincludeSubsidiaries\b/g, 'Include Regional Subsidiaries')
|
|
189
|
+
.replace(/\bincludeCreativeDetails\b/g, 'Include Creative Region & Legal Details')
|
|
190
|
+
.replace(/\bmaxEntities\b/g, 'Max Advertiser Entities')
|
|
191
|
+
.replace(/\bpageToken\b/g, 'Next-Page Cursor Token');
|
|
192
|
+
if (code === -32602) {
|
|
193
|
+
return `Invalid input: ${cleanMsg}`;
|
|
194
|
+
}
|
|
195
|
+
if (code === -32601) {
|
|
196
|
+
return `Unsupported action: ${cleanMsg}`;
|
|
197
|
+
}
|
|
198
|
+
return `Request error (${code || 'RPC'}): ${cleanMsg}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getHumanToolTitle(toolName) {
|
|
202
|
+
return (
|
|
203
|
+
TOOL_UI_META[toolName]?.title ||
|
|
204
|
+
state.tools.find((t) => t.name === toolName)?.title ||
|
|
205
|
+
toolName
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getSelectedToolSchema() {
|
|
210
|
+
return (
|
|
211
|
+
state.tools.find((t) => t.name === state.selectedToolName) ||
|
|
212
|
+
state.tools[0] ||
|
|
213
|
+
null
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resetStalePaginationCursorOnFilterEdit(changedFieldName) {
|
|
218
|
+
if (changedFieldName === 'pageToken') return;
|
|
219
|
+
const pageTokenInput = document.getElementById('field-pageToken');
|
|
220
|
+
if (pageTokenInput && pageTokenInput.value) {
|
|
221
|
+
pageTokenInput.value = '';
|
|
222
|
+
}
|
|
223
|
+
state.lastNextPageToken = null;
|
|
224
|
+
if (els.nextPageBtn) {
|
|
225
|
+
els.nextPageBtn.hidden = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function renderSchemaFields() {
|
|
230
|
+
clearChildren(els.dynamicFields);
|
|
231
|
+
const tool = getSelectedToolSchema();
|
|
232
|
+
if (!tool || !tool.inputSchema || !tool.inputSchema.properties) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const props = tool.inputSchema.properties;
|
|
237
|
+
const defaultValues = {
|
|
238
|
+
query: 'nike.com',
|
|
239
|
+
countryCode: '',
|
|
240
|
+
formatFilter: 'ALL',
|
|
241
|
+
limit: '5',
|
|
242
|
+
includeSubsidiaries: true,
|
|
243
|
+
includeCreativeDetails: false,
|
|
244
|
+
maxEntities: '3',
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
for (const [fieldName, fieldSpec] of Object.entries(props)) {
|
|
248
|
+
if (fieldName === 'maxResults') {
|
|
249
|
+
// Deprecated alias for limit; keep UI clean by exposing canonical limit control
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const uiFieldMeta = TOOL_UI_META[state.selectedToolName]?.fields?.[fieldName] || {};
|
|
254
|
+
const fieldTitle = uiFieldMeta.title || fieldSpec.title || fieldName;
|
|
255
|
+
const fieldHint = uiFieldMeta.hint || fieldSpec.description || '';
|
|
256
|
+
const fieldPlaceholder = uiFieldMeta.placeholder || fieldTitle;
|
|
257
|
+
|
|
258
|
+
const group = createEl('div', 'field-group');
|
|
259
|
+
const inputId = `field-${fieldName}`;
|
|
260
|
+
|
|
261
|
+
if (fieldSpec.type === 'boolean') {
|
|
262
|
+
const label = createEl('label', 'checkbox-row');
|
|
263
|
+
label.setAttribute('for', inputId);
|
|
264
|
+
const checkbox = createEl('input', 'checkbox-input');
|
|
265
|
+
checkbox.type = 'checkbox';
|
|
266
|
+
checkbox.id = inputId;
|
|
267
|
+
checkbox.name = fieldName;
|
|
268
|
+
checkbox.checked =
|
|
269
|
+
defaultValues[fieldName] !== undefined
|
|
270
|
+
? Boolean(defaultValues[fieldName])
|
|
271
|
+
: Boolean(fieldSpec.default);
|
|
272
|
+
checkbox.addEventListener('change', () => {
|
|
273
|
+
resetStalePaginationCursorOnFilterEdit(fieldName);
|
|
274
|
+
});
|
|
275
|
+
const span = createEl(
|
|
276
|
+
'span',
|
|
277
|
+
'',
|
|
278
|
+
fieldHint ? `${fieldTitle} - ${fieldHint}` : fieldTitle
|
|
279
|
+
);
|
|
280
|
+
label.appendChild(checkbox);
|
|
281
|
+
label.appendChild(span);
|
|
282
|
+
group.appendChild(label);
|
|
283
|
+
els.dynamicFields.appendChild(group);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const label = createEl('label', 'field-label', fieldTitle);
|
|
288
|
+
label.setAttribute('for', inputId);
|
|
289
|
+
group.appendChild(label);
|
|
290
|
+
|
|
291
|
+
if (Array.isArray(fieldSpec.enum) && fieldSpec.enum.length > 0) {
|
|
292
|
+
const select = createEl('select', 'select-control');
|
|
293
|
+
select.id = inputId;
|
|
294
|
+
select.name = fieldName;
|
|
295
|
+
for (const optionVal of fieldSpec.enum) {
|
|
296
|
+
const opt = createEl('option', '', optionVal);
|
|
297
|
+
opt.value = optionVal;
|
|
298
|
+
if (defaultValues[fieldName] === optionVal || fieldSpec.default === optionVal) {
|
|
299
|
+
opt.selected = true;
|
|
300
|
+
}
|
|
301
|
+
select.appendChild(opt);
|
|
302
|
+
}
|
|
303
|
+
select.addEventListener('change', () => {
|
|
304
|
+
resetStalePaginationCursorOnFilterEdit(fieldName);
|
|
305
|
+
});
|
|
306
|
+
group.appendChild(select);
|
|
307
|
+
} else {
|
|
308
|
+
const input = createEl('input', 'input-control');
|
|
309
|
+
input.id = inputId;
|
|
310
|
+
input.name = fieldName;
|
|
311
|
+
input.type = fieldSpec.type === 'integer' ? 'number' : 'text';
|
|
312
|
+
if (fieldSpec.minimum !== undefined) input.min = String(fieldSpec.minimum);
|
|
313
|
+
if (fieldSpec.maximum !== undefined) input.max = String(fieldSpec.maximum);
|
|
314
|
+
if (fieldSpec.maxLength !== undefined) input.maxLength = Number(fieldSpec.maxLength);
|
|
315
|
+
if (defaultValues[fieldName] !== undefined) {
|
|
316
|
+
input.value = String(defaultValues[fieldName]);
|
|
317
|
+
}
|
|
318
|
+
input.placeholder = fieldPlaceholder;
|
|
319
|
+
input.addEventListener('input', () => {
|
|
320
|
+
resetStalePaginationCursorOnFilterEdit(fieldName);
|
|
321
|
+
});
|
|
322
|
+
group.appendChild(input);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (fieldHint) {
|
|
326
|
+
const hint = createEl('div', 'field-hint', fieldHint);
|
|
327
|
+
group.appendChild(hint);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
els.dynamicFields.appendChild(group);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function collectToolArguments() {
|
|
335
|
+
const tool = getSelectedToolSchema();
|
|
336
|
+
if (!tool || !tool.inputSchema || !tool.inputSchema.properties) {
|
|
337
|
+
return {};
|
|
338
|
+
}
|
|
339
|
+
const args = {};
|
|
340
|
+
for (const [fieldName, fieldSpec] of Object.entries(tool.inputSchema.properties)) {
|
|
341
|
+
const el = document.getElementById(`field-${fieldName}`);
|
|
342
|
+
if (!el) continue;
|
|
343
|
+
if (fieldSpec.type === 'boolean') {
|
|
344
|
+
args[fieldName] = Boolean(el.checked);
|
|
345
|
+
} else if (fieldSpec.type === 'integer') {
|
|
346
|
+
const raw = el.value.trim();
|
|
347
|
+
if (raw !== '') {
|
|
348
|
+
args[fieldName] = Number(raw);
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
const raw = el.value.trim();
|
|
352
|
+
if (raw !== '') {
|
|
353
|
+
args[fieldName] = raw;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return args;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function renderResultCards(structured) {
|
|
361
|
+
clearChildren(els.cardsContainer);
|
|
362
|
+
if (!structured || typeof structured !== 'object') {
|
|
363
|
+
const emptyMsg = createEl('div', 'field-hint', 'No structured items to display.');
|
|
364
|
+
els.cardsContainer.appendChild(emptyMsg);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const items = Array.isArray(structured.items) ? structured.items : [];
|
|
369
|
+
const entities = Array.isArray(structured.selectedEntities) ? structured.selectedEntities : [];
|
|
370
|
+
|
|
371
|
+
if (items.length > 0) {
|
|
372
|
+
for (const item of items) {
|
|
373
|
+
const card = createEl('article', 'creative-card');
|
|
374
|
+
const header = createEl('div', 'creative-card-header');
|
|
375
|
+
const title = createEl(
|
|
376
|
+
'div',
|
|
377
|
+
'creative-title',
|
|
378
|
+
`${item.advertiserName || item.advertiserId} - ${item.creativeId}`
|
|
379
|
+
);
|
|
380
|
+
const formatBadge = createEl('span', 'badge badge-cyan', item.format || 'UNKNOWN');
|
|
381
|
+
header.appendChild(title);
|
|
382
|
+
header.appendChild(formatBadge);
|
|
383
|
+
card.appendChild(header);
|
|
384
|
+
|
|
385
|
+
const rawRegion = item.country || item.regionCode || item.targetCountry || 'ALL';
|
|
386
|
+
const regionLabel =
|
|
387
|
+
item.countrySource === 'QUERY_CONTEXT_FALLBACK' ||
|
|
388
|
+
item.countrySource === 'ENTITY_REGION_FALLBACK'
|
|
389
|
+
? `${rawRegion} (inferred)`
|
|
390
|
+
: item.countrySource === 'DEFAULT_ALL'
|
|
391
|
+
? `${rawRegion} (default)`
|
|
392
|
+
: rawRegion;
|
|
393
|
+
const daysActiveLabel =
|
|
394
|
+
item.daysActive !== null && item.daysActive !== undefined ? item.daysActive : 'N/A';
|
|
395
|
+
const firstSeenLabel = item.firstSeenAtIso || item.firstSeenAt || 'N/A';
|
|
396
|
+
|
|
397
|
+
const meta = createEl(
|
|
398
|
+
'div',
|
|
399
|
+
'creative-meta',
|
|
400
|
+
`Advertiser: ${item.advertiserId} | Region: ${regionLabel} | Days Active: ${daysActiveLabel} | First Seen: ${firstSeenLabel}`
|
|
401
|
+
);
|
|
402
|
+
card.appendChild(meta);
|
|
403
|
+
|
|
404
|
+
const adContent =
|
|
405
|
+
item.untrustedAdContent && typeof item.untrustedAdContent === 'object'
|
|
406
|
+
? item.untrustedAdContent
|
|
407
|
+
: item.creativeDetail &&
|
|
408
|
+
item.creativeDetail.untrustedAdContent &&
|
|
409
|
+
typeof item.creativeDetail.untrustedAdContent === 'object'
|
|
410
|
+
? item.creativeDetail.untrustedAdContent
|
|
411
|
+
: null;
|
|
412
|
+
|
|
413
|
+
if (adContent && adContent.extractionStatus === 'EXTRACTED') {
|
|
414
|
+
const copyBox = createEl('div', 'creative-ad-copy');
|
|
415
|
+
const copyHeader = createEl('div', 'creative-ad-copy-header');
|
|
416
|
+
copyHeader.appendChild(
|
|
417
|
+
createEl('span', 'badge badge-amber', 'Untrusted Ad Copy - Data Only')
|
|
418
|
+
);
|
|
419
|
+
if (adContent.callToAction) {
|
|
420
|
+
copyHeader.appendChild(
|
|
421
|
+
createEl('span', 'badge badge-cyan', `CTA: ${adContent.callToAction}`)
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
copyBox.appendChild(copyHeader);
|
|
425
|
+
|
|
426
|
+
if (adContent.headline) {
|
|
427
|
+
copyBox.appendChild(createEl('div', 'creative-ad-headline', adContent.headline));
|
|
428
|
+
}
|
|
429
|
+
if (adContent.bodyText) {
|
|
430
|
+
copyBox.appendChild(createEl('div', 'creative-ad-body', adContent.bodyText));
|
|
431
|
+
}
|
|
432
|
+
if (adContent.destinationDomain || adContent.landingPageUrl) {
|
|
433
|
+
const destLabel = adContent.destinationDomain
|
|
434
|
+
? `Destination Domain: ${adContent.destinationDomain}${
|
|
435
|
+
adContent.landingPageUrl ? ` (${adContent.landingPageUrl})` : ''
|
|
436
|
+
}`
|
|
437
|
+
: `Landing URL (text only): ${adContent.landingPageUrl}`;
|
|
438
|
+
copyBox.appendChild(createEl('div', 'creative-ad-dest', destLabel));
|
|
439
|
+
}
|
|
440
|
+
if (Array.isArray(adContent.omittedFields) && adContent.omittedFields.length > 0) {
|
|
441
|
+
copyBox.appendChild(
|
|
442
|
+
createEl(
|
|
443
|
+
'div',
|
|
444
|
+
'field-hint',
|
|
445
|
+
`Trimmed fields to fit 30 KB limit: ${adContent.omittedFields.join(', ')}`
|
|
446
|
+
)
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
card.appendChild(copyBox);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const linksRow = createEl('div', 'creative-links');
|
|
453
|
+
if (isSafeExternalAssetUrl(item.transparencyUrl)) {
|
|
454
|
+
const link = createEl('a', 'btn btn-secondary', 'View on Transparency Center');
|
|
455
|
+
link.href = item.transparencyUrl;
|
|
456
|
+
link.target = '_blank';
|
|
457
|
+
link.rel = 'noopener noreferrer';
|
|
458
|
+
linksRow.appendChild(link);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (state.loadPreviewsOptIn && isSafeExternalAssetUrl(item.previewUrl)) {
|
|
462
|
+
const previewLink = createEl('a', 'btn btn-secondary', 'Open Verified Preview Script');
|
|
463
|
+
previewLink.href = item.previewUrl;
|
|
464
|
+
previewLink.target = '_blank';
|
|
465
|
+
previewLink.rel = 'noopener noreferrer';
|
|
466
|
+
linksRow.appendChild(previewLink);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (
|
|
470
|
+
state.loadPreviewsOptIn &&
|
|
471
|
+
Array.isArray(item.archiveImageUrls) &&
|
|
472
|
+
item.archiveImageUrls.length > 0 &&
|
|
473
|
+
isSafeExternalAssetUrl(item.archiveImageUrls[0])
|
|
474
|
+
) {
|
|
475
|
+
const archiveLink = createEl('a', 'btn btn-secondary', 'Open Archive Image');
|
|
476
|
+
archiveLink.href = item.archiveImageUrls[0];
|
|
477
|
+
archiveLink.target = '_blank';
|
|
478
|
+
archiveLink.rel = 'noopener noreferrer';
|
|
479
|
+
linksRow.appendChild(archiveLink);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
card.appendChild(linksRow);
|
|
483
|
+
els.cardsContainer.appendChild(card);
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (entities.length > 0) {
|
|
489
|
+
for (const entity of entities) {
|
|
490
|
+
const card = createEl('article', 'creative-card');
|
|
491
|
+
const header = createEl('div', 'creative-card-header');
|
|
492
|
+
const title = createEl(
|
|
493
|
+
'div',
|
|
494
|
+
'creative-title',
|
|
495
|
+
`${entity.advertiserName} (${entity.advertiserId})`
|
|
496
|
+
);
|
|
497
|
+
const scoreBadge = createEl(
|
|
498
|
+
'span',
|
|
499
|
+
'badge badge-emerald',
|
|
500
|
+
`Score: ${entity.rankingScore}`
|
|
501
|
+
);
|
|
502
|
+
header.appendChild(title);
|
|
503
|
+
header.appendChild(scoreBadge);
|
|
504
|
+
card.appendChild(header);
|
|
505
|
+
|
|
506
|
+
const meta = createEl(
|
|
507
|
+
'div',
|
|
508
|
+
'creative-meta',
|
|
509
|
+
`Region: ${entity.regionCode || 'ALL'} | Ad Volume Bucket: ${entity.minAds}..${entity.maxAds} | Primary Corporate: ${entity.isPrimaryCorporateEntity ? 'Yes' : 'No'}`
|
|
510
|
+
);
|
|
511
|
+
card.appendChild(meta);
|
|
512
|
+
els.cardsContainer.appendChild(card);
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const emptyNotice = createEl(
|
|
518
|
+
'div',
|
|
519
|
+
'field-hint',
|
|
520
|
+
`Status: ${structured.status || 'EMPTY_RESULT'} (0 items matched)`
|
|
521
|
+
);
|
|
522
|
+
els.cardsContainer.appendChild(emptyNotice);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function copyExportableJson() {
|
|
526
|
+
if (!state.lastResultPayload || typeof state.lastResultPayload !== 'object') {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const tokenAtCopyStart = state.activeRequestToken;
|
|
530
|
+
const serialized = JSON.stringify(state.lastResultPayload, null, 2);
|
|
531
|
+
let copied = false;
|
|
532
|
+
try {
|
|
533
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
534
|
+
await navigator.clipboard.writeText(serialized);
|
|
535
|
+
copied = true;
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
copied = false;
|
|
539
|
+
}
|
|
540
|
+
if (tokenAtCopyStart !== state.activeRequestToken) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (copied) {
|
|
544
|
+
els.statusBanner.dataset.state = 'ok';
|
|
545
|
+
els.statusBanner.textContent = `Copied structured JSON (${serialized.length} chars) to clipboard.`;
|
|
546
|
+
} else {
|
|
547
|
+
els.statusBanner.dataset.state = 'warn';
|
|
548
|
+
els.statusBanner.textContent = `Clipboard copy unavailable (${serialized.length} chars) - use Download JSON or copy from the box below.`;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function downloadExportableJson() {
|
|
553
|
+
if (!state.lastResultPayload || typeof state.lastResultPayload !== 'object') {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const serialized = JSON.stringify(state.lastResultPayload, null, 2);
|
|
557
|
+
const filename = 'revclamp-adpulse-output.json';
|
|
558
|
+
const blob = new Blob([serialized], { type: 'application/json;charset=utf-8' });
|
|
559
|
+
const url = URL.createObjectURL(blob);
|
|
560
|
+
const a = document.createElement('a');
|
|
561
|
+
a.href = url;
|
|
562
|
+
a.download = filename;
|
|
563
|
+
document.body.appendChild(a);
|
|
564
|
+
a.click();
|
|
565
|
+
document.body.removeChild(a);
|
|
566
|
+
setTimeout(() => {
|
|
567
|
+
URL.revokeObjectURL(url);
|
|
568
|
+
}, 1000);
|
|
569
|
+
els.statusBanner.dataset.state = 'ok';
|
|
570
|
+
els.statusBanner.textContent = `Download started for ${filename} (${serialized.length} chars).`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function executeSelectedTool() {
|
|
574
|
+
const requestToken = ++state.activeRequestToken;
|
|
575
|
+
clearExportableResultState(true, true);
|
|
576
|
+
if (els.rawJsonBox) {
|
|
577
|
+
els.rawJsonBox.textContent = '{}';
|
|
578
|
+
}
|
|
579
|
+
state.lastNextPageToken = null;
|
|
580
|
+
if (els.nextPageBtn) {
|
|
581
|
+
els.nextPageBtn.hidden = true;
|
|
582
|
+
}
|
|
583
|
+
const args = collectToolArguments();
|
|
584
|
+
const reqId = `ui_${state.rpcRequestId++}`;
|
|
585
|
+
const toolNameAtStart = state.selectedToolName;
|
|
586
|
+
const humanToolLabel = getHumanToolTitle(toolNameAtStart);
|
|
587
|
+
els.statusBanner.dataset.state = 'pending';
|
|
588
|
+
els.statusBanner.textContent = `Running ${humanToolLabel}...`;
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
const response = await fetch('/api/mcp', {
|
|
592
|
+
method: 'POST',
|
|
593
|
+
headers: {
|
|
594
|
+
'content-type': 'application/json',
|
|
595
|
+
},
|
|
596
|
+
body: JSON.stringify({
|
|
597
|
+
jsonrpc: '2.0',
|
|
598
|
+
id: reqId,
|
|
599
|
+
method: 'tools/call',
|
|
600
|
+
params: {
|
|
601
|
+
name: toolNameAtStart,
|
|
602
|
+
arguments: args,
|
|
603
|
+
},
|
|
604
|
+
}),
|
|
605
|
+
});
|
|
606
|
+
if (requestToken !== state.activeRequestToken) return;
|
|
607
|
+
|
|
608
|
+
const rawText = await response.text();
|
|
609
|
+
if (requestToken !== state.activeRequestToken) return;
|
|
610
|
+
|
|
611
|
+
const byteLength = new TextEncoder().encode(rawText).length;
|
|
612
|
+
els.envelopeBytesValue.textContent = `${byteLength} B`;
|
|
613
|
+
|
|
614
|
+
let rpcEnvelope;
|
|
615
|
+
try {
|
|
616
|
+
rpcEnvelope = JSON.parse(rawText);
|
|
617
|
+
} catch {
|
|
618
|
+
clearExportableResultState(true, false);
|
|
619
|
+
state.lastNextPageToken = null;
|
|
620
|
+
els.nextPageBtn.hidden = true;
|
|
621
|
+
els.statusBanner.dataset.state = 'error';
|
|
622
|
+
els.statusBanner.textContent = `HTTP ${response.status}: Non-JSON response received.`;
|
|
623
|
+
els.rawJsonBox.textContent = rawText;
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
els.rawJsonBox.textContent = JSON.stringify(rpcEnvelope, null, 2);
|
|
628
|
+
|
|
629
|
+
if (rpcEnvelope.ok === false && typeof rpcEnvelope.error === 'string') {
|
|
630
|
+
clearExportableResultState(true, false);
|
|
631
|
+
els.statusBanner.dataset.state = 'error';
|
|
632
|
+
els.statusBanner.textContent = `HTTP ${response.status} Error: ${rpcEnvelope.error}`;
|
|
633
|
+
state.lastNextPageToken = null;
|
|
634
|
+
els.nextPageBtn.hidden = true;
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (rpcEnvelope.error) {
|
|
639
|
+
clearExportableResultState(true, false);
|
|
640
|
+
els.statusBanner.dataset.state = 'error';
|
|
641
|
+
els.statusBanner.textContent = humanizeRpcErrorMessage(rpcEnvelope.error, toolNameAtStart);
|
|
642
|
+
state.lastNextPageToken = null;
|
|
643
|
+
els.nextPageBtn.hidden = true;
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const rawStructured = rpcEnvelope.result?.structuredContent;
|
|
648
|
+
const hasValidStructured = Boolean(
|
|
649
|
+
rawStructured && typeof rawStructured === 'object' && !Array.isArray(rawStructured)
|
|
650
|
+
);
|
|
651
|
+
const structured = hasValidStructured ? rawStructured : {};
|
|
652
|
+
const isResultError = Boolean(rpcEnvelope.result?.isError);
|
|
653
|
+
if (!isResultError && hasValidStructured) {
|
|
654
|
+
state.lastResultPayload = structured;
|
|
655
|
+
setExportButtonsEnabled(true);
|
|
656
|
+
} else {
|
|
657
|
+
clearExportableResultState(true, false);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const chargedCount = Number(structured.chargedEventCount || 0);
|
|
661
|
+
els.billingModeValue.textContent =
|
|
662
|
+
chargedCount > 0
|
|
663
|
+
? `Confirmed charged (${chargedCount})`
|
|
664
|
+
: 'Local mode - not billed';
|
|
665
|
+
|
|
666
|
+
const countVal = Array.isArray(structured.items) && structured.items.length > 0
|
|
667
|
+
? structured.items.length
|
|
668
|
+
: structured.deliveredCount !== undefined
|
|
669
|
+
? structured.deliveredCount
|
|
670
|
+
: structured.extractedCount !== undefined
|
|
671
|
+
? structured.extractedCount
|
|
672
|
+
: Array.isArray(structured.selectedEntities)
|
|
673
|
+
? structured.selectedEntities.length
|
|
674
|
+
: 0;
|
|
675
|
+
els.extractedCountValue.textContent = String(countVal);
|
|
676
|
+
|
|
677
|
+
const pagStatus =
|
|
678
|
+
structured.pagination?.paginationStatus || structured.status || 'OK';
|
|
679
|
+
els.paginationStatusValue.textContent = pagStatus;
|
|
680
|
+
|
|
681
|
+
const nextToken = structured.pagination?.nextPageToken || null;
|
|
682
|
+
state.lastNextPageToken = nextToken;
|
|
683
|
+
els.nextPageBtn.hidden = !nextToken;
|
|
684
|
+
|
|
685
|
+
const omittedCount = Number(structured.omittedItemCount ?? structured.omittedCount ?? 0);
|
|
686
|
+
const isByteTruncated = Boolean(structured.truncatedForMcpByteCeiling);
|
|
687
|
+
const isPartialMultiPage =
|
|
688
|
+
pagStatus === 'PARTIAL_PAGES_HAS_MORE' || pagStatus === 'DEADLINE_REACHED_HAS_MORE';
|
|
689
|
+
const truncationSuffix =
|
|
690
|
+
omittedCount > 0
|
|
691
|
+
? nextToken
|
|
692
|
+
? ` (${omittedCount} omitted to fit the 30 KB limit - click Load next page to continue).`
|
|
693
|
+
: ` (${omittedCount} omitted to fit the 30 KB limit).`
|
|
694
|
+
: isByteTruncated
|
|
695
|
+
? ' (some details were trimmed to fit the 30 KB limit).'
|
|
696
|
+
: isPartialMultiPage
|
|
697
|
+
? ' (partial multi-page results preserved - click Load next page to continue).'
|
|
698
|
+
: '.';
|
|
699
|
+
|
|
700
|
+
els.statusBanner.dataset.state = isResultError
|
|
701
|
+
? 'error'
|
|
702
|
+
: isPartialMultiPage
|
|
703
|
+
? 'warn'
|
|
704
|
+
: 'ok';
|
|
705
|
+
els.statusBanner.textContent = isResultError
|
|
706
|
+
? `Run halted (${structured.status || 'ERROR'}): ${structured.reason || 'Upstream error occurred.'}`
|
|
707
|
+
: `Completed ${humanToolLabel} (${countVal} matching ${countVal === 1 ? 'record' : 'records'})${truncationSuffix}`;
|
|
708
|
+
|
|
709
|
+
renderResultCards(structured);
|
|
710
|
+
} catch (err) {
|
|
711
|
+
if (requestToken !== state.activeRequestToken) return;
|
|
712
|
+
clearExportableResultState(true, false);
|
|
713
|
+
if (els.rawJsonBox) {
|
|
714
|
+
els.rawJsonBox.textContent = '{}';
|
|
715
|
+
}
|
|
716
|
+
state.lastNextPageToken = null;
|
|
717
|
+
els.nextPageBtn.hidden = true;
|
|
718
|
+
els.statusBanner.dataset.state = 'error';
|
|
719
|
+
els.statusBanner.textContent = `Request failed: ${err?.message || String(err)}`;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function initPlayground() {
|
|
724
|
+
try {
|
|
725
|
+
const res = await fetch('/api/schema');
|
|
726
|
+
const data = await res.json();
|
|
727
|
+
state.tools = Array.isArray(data.tools) ? data.tools : [];
|
|
728
|
+
} catch {
|
|
729
|
+
state.tools = [];
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
clearChildren(els.toolSelect);
|
|
733
|
+
for (const tool of state.tools) {
|
|
734
|
+
const optLabel = TOOL_UI_META[tool.name]?.title || tool.title || tool.name;
|
|
735
|
+
const opt = createEl('option', '', optLabel);
|
|
736
|
+
opt.value = tool.name;
|
|
737
|
+
if (tool.name === state.selectedToolName) {
|
|
738
|
+
opt.selected = true;
|
|
739
|
+
}
|
|
740
|
+
els.toolSelect.appendChild(opt);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
renderSchemaFields();
|
|
744
|
+
|
|
745
|
+
els.toolSelect.addEventListener('change', () => {
|
|
746
|
+
state.activeRequestToken += 1;
|
|
747
|
+
state.selectedToolName = els.toolSelect.value;
|
|
748
|
+
state.lastNextPageToken = null;
|
|
749
|
+
els.nextPageBtn.hidden = true;
|
|
750
|
+
clearExportableResultState(true, true);
|
|
751
|
+
els.rawJsonBox.textContent = '{}';
|
|
752
|
+
els.statusBanner.dataset.state = 'idle';
|
|
753
|
+
els.statusBanner.textContent = `Ready to run ${getHumanToolTitle(state.selectedToolName)}.`;
|
|
754
|
+
renderSchemaFields();
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
els.loadPreviewsCheckbox.addEventListener('change', () => {
|
|
758
|
+
state.loadPreviewsOptIn = Boolean(els.loadPreviewsCheckbox.checked);
|
|
759
|
+
if (state.lastResultPayload) {
|
|
760
|
+
renderResultCards(state.lastResultPayload);
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
els.runBtn.addEventListener('click', () => {
|
|
765
|
+
executeSelectedTool();
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
els.nextPageBtn.addEventListener('click', () => {
|
|
769
|
+
if (!state.lastNextPageToken) return;
|
|
770
|
+
const pageTokenInput = document.getElementById('field-pageToken');
|
|
771
|
+
if (pageTokenInput) {
|
|
772
|
+
pageTokenInput.value = state.lastNextPageToken;
|
|
773
|
+
}
|
|
774
|
+
executeSelectedTool();
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
if (els.copyJsonBtn) {
|
|
778
|
+
els.copyJsonBtn.addEventListener('click', () => {
|
|
779
|
+
copyExportableJson();
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (els.downloadJsonBtn) {
|
|
784
|
+
els.downloadJsonBtn.addEventListener('click', () => {
|
|
785
|
+
downloadExportableJson();
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (document.readyState === 'loading') {
|
|
791
|
+
document.addEventListener('DOMContentLoaded', initPlayground);
|
|
792
|
+
} else {
|
|
793
|
+
initPlayground();
|
|
794
|
+
}
|
|
795
|
+
})();
|