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.
Files changed (2) hide show
  1. package/dist/index.js +224 -0
  2. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "optimizely-mcp-server",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "MCP server for the Optimizely REST API — manage experiments, feature flags, audiences, and more from any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",