optimizely-mcp-server 1.0.0 → 1.0.2
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/dist/index.js +224 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -360,6 +360,230 @@ server.tool("create_flag_variation", "Create a new variation for a feature flag
|
|
|
360
360
|
});
|
|
361
361
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
362
362
|
});
|
|
363
|
+
// ─── Create Full Test (auto-discovers required_ metrics) ────────────────────
|
|
364
|
+
const WEB_VITALS = new Set(["required_fcp", "required_lcp", "required_ttfb", "required_cls", "required_inp"]);
|
|
365
|
+
const AD_LOADTIME_KEYS = new Set(["leaderboard_ad_loadtime"]);
|
|
366
|
+
function classifyEvent(evt) {
|
|
367
|
+
const key = evt.key;
|
|
368
|
+
const metrics = [];
|
|
369
|
+
if (key === "required_o_ad_slot_views") {
|
|
370
|
+
metrics.push({
|
|
371
|
+
event_id: evt.id,
|
|
372
|
+
aggregator: "count",
|
|
373
|
+
scope: "visitor",
|
|
374
|
+
winning_direction: "increasing",
|
|
375
|
+
display_title: key,
|
|
376
|
+
});
|
|
377
|
+
metrics.push({
|
|
378
|
+
event_id: evt.id,
|
|
379
|
+
aggregator: "sum",
|
|
380
|
+
scope: "visitor",
|
|
381
|
+
winning_direction: "increasing",
|
|
382
|
+
display_title: "ad impression value",
|
|
383
|
+
field: "value",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
else if (WEB_VITALS.has(key)) {
|
|
387
|
+
metrics.push({
|
|
388
|
+
event_id: evt.id,
|
|
389
|
+
aggregator: "sum",
|
|
390
|
+
scope: "event",
|
|
391
|
+
winning_direction: "decreasing",
|
|
392
|
+
display_title: key,
|
|
393
|
+
field: "value",
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
else if (AD_LOADTIME_KEYS.has(key)) {
|
|
397
|
+
metrics.push({
|
|
398
|
+
event_id: evt.id,
|
|
399
|
+
aggregator: "sum",
|
|
400
|
+
scope: "event",
|
|
401
|
+
winning_direction: "decreasing",
|
|
402
|
+
display_title: key,
|
|
403
|
+
field: "value",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
else if (key.startsWith("required_o_") || key === "required_pages_visited") {
|
|
407
|
+
metrics.push({
|
|
408
|
+
event_id: evt.id,
|
|
409
|
+
aggregator: "sum",
|
|
410
|
+
scope: "visitor",
|
|
411
|
+
winning_direction: "increasing",
|
|
412
|
+
display_title: key,
|
|
413
|
+
field: "value",
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
else if (key.startsWith("required_saw_") || key.startsWith("required_finished_")) {
|
|
417
|
+
metrics.push({
|
|
418
|
+
event_id: evt.id,
|
|
419
|
+
aggregator: "unique",
|
|
420
|
+
scope: "visitor",
|
|
421
|
+
winning_direction: "increasing",
|
|
422
|
+
display_title: key,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
else if (key.startsWith("required_")) {
|
|
426
|
+
metrics.push({
|
|
427
|
+
event_id: evt.id,
|
|
428
|
+
aggregator: "unique",
|
|
429
|
+
scope: "visitor",
|
|
430
|
+
winning_direction: "increasing",
|
|
431
|
+
display_title: key,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return metrics;
|
|
435
|
+
}
|
|
436
|
+
async function fetchAllEvents(projectId) {
|
|
437
|
+
const allEvents = [];
|
|
438
|
+
let page = 1;
|
|
439
|
+
while (true) {
|
|
440
|
+
const batch = await client.request({
|
|
441
|
+
path: "/v2/events",
|
|
442
|
+
params: { project_id: projectId, per_page: 100, page },
|
|
443
|
+
});
|
|
444
|
+
if (!Array.isArray(batch) || batch.length === 0)
|
|
445
|
+
break;
|
|
446
|
+
allEvents.push(...batch);
|
|
447
|
+
if (batch.length < 100)
|
|
448
|
+
break;
|
|
449
|
+
page++;
|
|
450
|
+
}
|
|
451
|
+
return allEvents;
|
|
452
|
+
}
|
|
453
|
+
async function buildStandardMetrics(projectId) {
|
|
454
|
+
const events = await fetchAllEvents(projectId);
|
|
455
|
+
const metrics = [];
|
|
456
|
+
for (const evt of events) {
|
|
457
|
+
if (evt.key.startsWith("required_") || AD_LOADTIME_KEYS.has(evt.key)) {
|
|
458
|
+
metrics.push(...classifyEvent(evt));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return metrics;
|
|
462
|
+
}
|
|
463
|
+
server.tool("create_full_test", "Create a fully configured A/B test with a feature flag, Off/On variations, and all standard required_ metrics auto-discovered from the project. Sets up rules in both development and production environments. This replicates what the internal dashboard does.", {
|
|
464
|
+
project_id: z.number().describe("The project ID"),
|
|
465
|
+
flag_key: z.string().describe("Unique flag key (alphanumeric, underscores, hyphens)"),
|
|
466
|
+
flag_name: z.string().describe("Human-readable name (typically the Jira ticket, e.g. 'FFD-8700: My Feature Test')"),
|
|
467
|
+
rule_name: z.string().optional().describe("Name for the A/B test rule (defaults to flag_key)"),
|
|
468
|
+
primary_metric_key: z.string().optional().describe("Key of the primary metric event (defaults to 'required_o_ad_slot_views')"),
|
|
469
|
+
traffic_allocation: z.number().optional().describe("Traffic allocation percentage (0-10000 basis points, default 100 = 1%)"),
|
|
470
|
+
extra_metrics: z
|
|
471
|
+
.array(z.object({
|
|
472
|
+
event_name: z.string().describe("Event name/key to search for"),
|
|
473
|
+
aggregator: z.enum(["unique", "count", "sum"]).optional().describe("Aggregator (default unique)"),
|
|
474
|
+
scope: z.enum(["visitor", "event"]).optional().describe("Scope (default visitor)"),
|
|
475
|
+
winning_direction: z.enum(["increasing", "decreasing"]).optional().describe("Winning direction (default increasing)"),
|
|
476
|
+
field: z.string().optional().describe("Field name for sum aggregator (e.g. 'value')"),
|
|
477
|
+
}))
|
|
478
|
+
.optional()
|
|
479
|
+
.describe("Additional test-specific metrics beyond the standard required_ set"),
|
|
480
|
+
}, async ({ project_id, flag_key, flag_name, rule_name, primary_metric_key, traffic_allocation, extra_metrics }) => {
|
|
481
|
+
const results = [];
|
|
482
|
+
const ruleName = rule_name ?? flag_key;
|
|
483
|
+
const primaryKey = primary_metric_key ?? "required_o_ad_slot_views";
|
|
484
|
+
const traffic = traffic_allocation ?? 100;
|
|
485
|
+
// 1. Create the feature flag
|
|
486
|
+
const flag = await client.request({
|
|
487
|
+
method: "POST",
|
|
488
|
+
path: `/flags/v1/projects/${project_id}/flags`,
|
|
489
|
+
body: { key: flag_key, name: flag_name },
|
|
490
|
+
});
|
|
491
|
+
results.push(`Created flag: ${flag_key} (${flag_name})`);
|
|
492
|
+
// 2. Auto-discover standard metrics
|
|
493
|
+
const standardMetrics = await buildStandardMetrics(project_id);
|
|
494
|
+
results.push(`Auto-discovered ${standardMetrics.length} standard metrics from required_ events`);
|
|
495
|
+
// 3. Resolve extra metrics by matching event names
|
|
496
|
+
const allMetrics = [...standardMetrics];
|
|
497
|
+
if (extra_metrics && extra_metrics.length > 0) {
|
|
498
|
+
const events = await fetchAllEvents(project_id);
|
|
499
|
+
for (const em of extra_metrics) {
|
|
500
|
+
const match = events.find((e) => e.key === em.event_name || e.name === em.event_name);
|
|
501
|
+
if (match) {
|
|
502
|
+
const m = {
|
|
503
|
+
event_id: match.id,
|
|
504
|
+
aggregator: em.aggregator ?? "unique",
|
|
505
|
+
scope: em.scope ?? "visitor",
|
|
506
|
+
winning_direction: em.winning_direction ?? "increasing",
|
|
507
|
+
display_title: em.event_name,
|
|
508
|
+
};
|
|
509
|
+
if (em.field)
|
|
510
|
+
m.field = em.field;
|
|
511
|
+
allMetrics.unshift(m);
|
|
512
|
+
results.push(`Added extra metric: ${em.event_name}`);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
results.push(`Warning: event '${em.event_name}' not found, skipped`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// 4. Find the primary metric event ID
|
|
520
|
+
const events = await fetchAllEvents(project_id);
|
|
521
|
+
const primaryEvent = events.find((e) => e.key === primaryKey);
|
|
522
|
+
const primaryMetricDisplay = primaryKey;
|
|
523
|
+
// 5. Create A/B rule in both environments
|
|
524
|
+
for (const env of ["development", "production"]) {
|
|
525
|
+
const envRuleName = env === "production" ? `${ruleName}prod` : ruleName;
|
|
526
|
+
try {
|
|
527
|
+
await client.request({
|
|
528
|
+
method: "POST",
|
|
529
|
+
path: `/flags/v1/projects/${project_id}/flags/${flag_key}/environments/${env}/rules`,
|
|
530
|
+
body: {
|
|
531
|
+
key: ruleName,
|
|
532
|
+
name: envRuleName,
|
|
533
|
+
type: "a/b",
|
|
534
|
+
traffic_allocation: traffic,
|
|
535
|
+
distribution_mode: "manual",
|
|
536
|
+
variations: [
|
|
537
|
+
{ key: "off", name: "Off", weight: 5000 },
|
|
538
|
+
{ key: "on", name: "On", weight: 5000 },
|
|
539
|
+
],
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
results.push(`Created A/B rule in ${env}: ${envRuleName}`);
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
results.push(`Warning: Could not create rule in ${env}: ${err}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// 6. Attach metrics to the experiment(s)
|
|
549
|
+
const flagData = await client.request({
|
|
550
|
+
path: `/flags/v1/projects/${project_id}/flags/${flag_key}`,
|
|
551
|
+
});
|
|
552
|
+
const environments = flagData.environments ?? {};
|
|
553
|
+
for (const [envKey, envData] of Object.entries(environments)) {
|
|
554
|
+
const rules = envData.rules_detail ?? [];
|
|
555
|
+
for (const rule of rules) {
|
|
556
|
+
if (rule.layer_experiment_id) {
|
|
557
|
+
try {
|
|
558
|
+
await client.request({
|
|
559
|
+
method: "PATCH",
|
|
560
|
+
path: `/v2/experiments/${rule.layer_experiment_id}`,
|
|
561
|
+
body: { metrics: allMetrics },
|
|
562
|
+
});
|
|
563
|
+
results.push(`Attached ${allMetrics.length} metrics to ${envKey} experiment (${rule.layer_experiment_id})`);
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
results.push(`Warning: Could not attach metrics in ${envKey}: ${err}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const summary = [
|
|
572
|
+
`\n=== Test Created Successfully ===`,
|
|
573
|
+
`Flag: ${flag_key}`,
|
|
574
|
+
`Name: ${flag_name}`,
|
|
575
|
+
`Variations: Off / On`,
|
|
576
|
+
`Traffic: ${traffic / 100}%`,
|
|
577
|
+
`Primary Metric: ${primaryMetricDisplay}`,
|
|
578
|
+
`Standard Metrics: ${standardMetrics.length} (auto-discovered from required_ events)`,
|
|
579
|
+
`Extra Metrics: ${extra_metrics?.length ?? 0}`,
|
|
580
|
+
`Environments: development, production`,
|
|
581
|
+
``,
|
|
582
|
+
`--- Log ---`,
|
|
583
|
+
...results,
|
|
584
|
+
].join("\n");
|
|
585
|
+
return { content: [{ type: "text", text: summary }] };
|
|
586
|
+
});
|
|
363
587
|
// ─── Start the server ────────────────────────────────────────────────────────
|
|
364
588
|
async function main() {
|
|
365
589
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED