@convertcom/mcp-server 0.0.1-beta.16 → 0.0.1-beta.17

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/build/operator.js CHANGED
@@ -152,7 +152,7 @@ const WRITE_ACTIONS = [
152
152
  {
153
153
  action: 'create_split_url_test',
154
154
  mode: 'write',
155
- description: 'Create a draft split URL experience and verify the persisted redirect. Required body: original_pattern (or url) and variation_pattern (variation_url and redirect_url are accepted aliases). Optional: name, variation_name, case_sensitive, description, objective, primary_goal, goals, audiences, locations, settings, split_regex_support, or canonical variations with at most 100 changes each for bounded verification. Payload verification is independent from attached-goal lifecycle health; goal_configuration reports archived or unresolved goals without encouraging a duplicate create. Returns one authoritative experience plus a compact create receipt. Unknown fields are rejected.',
155
+ description: 'Create a draft split URL experience with an Original no-redirect control and a redirect treatment, allocated 50/50, and verify both arms, baseline, allocation and redirects. Required body: original_pattern (or url) and variation_pattern (variation_url and redirect_url are accepted aliases). Optional: name, variation_name, case_sensitive, description, objective, primary_goal, goals, audiences, locations, settings, split_regex_support. Custom variations require unique names, exactly one explicit is_baseline: true control with no changes, one defaultRedirect per treatment, and positive numeric traffic_distribution per arm totaling 100; at most 100 changes per variation. goal_configuration separately reports archived or unresolved goals. Returns one authoritative experience plus a create receipt. If verified is false, inspect that existing draft; never repeat create or activate it automatically. Unknown fields are rejected.',
156
156
  path_parameters: ['account_id', 'project_id'],
157
157
  accepts_body: true,
158
158
  },
@@ -519,16 +519,24 @@ function validateOperatorWriteInput(args) {
519
519
  if (args.action === 'create_split_url_test') {
520
520
  const original = args.body.original_pattern ?? args.body.url;
521
521
  const variation = args.body.variation_pattern ?? args.body.variation_url ?? args.body.redirect_url;
522
- const variationsIssue = validateCanonicalOperatorVariations(args.body.variations);
522
+ const variationsIssue = validateCanonicalOperatorVariations(args.body.variations, true);
523
523
  if (variationsIssue)
524
524
  return variationsIssue;
525
525
  if (typeof original !== 'string' || !original.trim()) {
526
526
  return 'operator.create_split_url_test requires body.original_pattern or body.url.';
527
527
  }
528
+ if (Array.isArray(args.body.variations)) {
529
+ const splitIssue = validateSplitUrlArms(args.body.variations);
530
+ if (splitIssue)
531
+ return splitIssue;
532
+ }
528
533
  if (!Array.isArray(args.body.variations)) {
529
534
  if (typeof variation !== 'string' || !variation.trim()) {
530
535
  return 'operator.create_split_url_test requires body.variation_pattern; body.variation_url and body.redirect_url are accepted aliases.';
531
536
  }
537
+ const splitIssue = validateSplitUrlArms(buildSplitUrlExperienceBody(args.body).variations);
538
+ if (splitIssue)
539
+ return splitIssue;
532
540
  }
533
541
  }
534
542
  if (args.action === 'enable_split_url_regex'
@@ -639,13 +647,15 @@ function feedbackActionConstraints(actions) {
639
647
  });
640
648
  return constraints;
641
649
  }
642
- function validateCanonicalOperatorVariations(value) {
650
+ function validateCanonicalOperatorVariations(value, allowBaseline = false) {
643
651
  if (typeof value === 'undefined')
644
652
  return null;
645
653
  if (!Array.isArray(value) || value.length === 0) {
646
654
  return 'body.variations must be a non-empty array when provided.';
647
655
  }
648
656
  const allowedVariationKeys = new Set(['name', 'status', 'traffic_distribution', 'traffic_percentage', 'changes']);
657
+ if (allowBaseline)
658
+ allowedVariationKeys.add('is_baseline');
649
659
  for (const [index, variation] of value.entries()) {
650
660
  if (!isObject(variation))
651
661
  return `body.variations[${index}] must be an object.`;
@@ -665,6 +675,28 @@ function validateCanonicalOperatorVariations(value) {
665
675
  }
666
676
  return null;
667
677
  }
678
+ function validateSplitUrlArms(variations) {
679
+ const hint = 'Split URL tests require at least two uniquely named arms, exactly one explicit is_baseline: true control with no changes, and a defaultRedirect change in each treatment. Supply numeric traffic_distribution for every arm (positive, totaling 100).';
680
+ if (variations.some((v) => !isObject(v) || typeof v.name !== 'string' || !v.name.trim()
681
+ || !Array.isArray(v.changes) || v.changes.some((change) => !isObject(change))))
682
+ return hint;
683
+ if (variations.length < 2 || new Set(variations.map((v) => v.name)).size !== variations.length)
684
+ return hint;
685
+ const controls = variations.filter((v) => v.is_baseline === true);
686
+ if (controls.length !== 1 || controls[0].changes.length !== 0)
687
+ return hint;
688
+ for (const variation of variations) {
689
+ if (typeof variation.traffic_distribution !== 'number' || !Number.isFinite(variation.traffic_distribution)
690
+ || variation.traffic_distribution <= 0 || variation.traffic_distribution > 100)
691
+ return hint;
692
+ if (variation.is_baseline !== true
693
+ && variation.changes.filter((change) => change.type === 'defaultRedirect').length !== 1)
694
+ return hint;
695
+ }
696
+ if (Math.abs(variations.reduce((sum, v) => sum + v.traffic_distribution, 0) - 100) > 0.00005)
697
+ return hint;
698
+ return null;
699
+ }
668
700
  function operatorVariationsContainCode(value) {
669
701
  if (!Array.isArray(value))
670
702
  return false;
@@ -1804,6 +1836,10 @@ async function createSplitUrlTest(args, context) {
1804
1836
  readback_status: readback?.status ?? null,
1805
1837
  },
1806
1838
  verified,
1839
+ ...(verified ? {} : {
1840
+ remediation_required: true,
1841
+ next_steps: ['Do not repeat create or activate this experience. Inspect the existing resource using create_receipt.experience_id; if the ID is unavailable, locate it before any further write. Correct the draft only after reviewing the persisted arms and allocation.'],
1842
+ }),
1807
1843
  goal_configuration: goalConfigurationSummary(goalStatus),
1808
1844
  ...(verified && verification.normalizedCodeWhitespace ? {
1809
1845
  verification_normalizations: [
@@ -1816,7 +1852,9 @@ async function createSplitUrlTest(args, context) {
1816
1852
  ? 'Readback confirmed the draft split URL experience and exact redirect patterns after Convert canonicalized only surrounding code whitespace.'
1817
1853
  : 'Readback confirmed the draft split URL experience and exact redirect patterns.'
1818
1854
  : 'Readback succeeded, but one or more requested split URL fields did not persist exactly.'
1819
- : 'Create call succeeded but no readback ID was available.',
1855
+ : experienceId
1856
+ ? 'Create succeeded, but readback failed. The existing draft is unverified; do not repeat create.'
1857
+ : 'Create succeeded but returned no usable experience ID. Locate the existing draft before any further write; do not repeat create.',
1820
1858
  };
1821
1859
  }
1822
1860
  async function enableSplitUrlRegex(args, context) {
@@ -2095,8 +2133,11 @@ export function buildSplitUrlExperienceBody(input) {
2095
2133
  }
2096
2134
  : input.settings,
2097
2135
  variations: Array.isArray(input.variations) ? input.variations : [
2136
+ { name: 'Original', is_baseline: true, traffic_distribution: 50, changes: [] },
2098
2137
  {
2099
2138
  name: stringFrom(input.variation_name, 'Variation URL'),
2139
+ is_baseline: false,
2140
+ traffic_distribution: 50,
2100
2141
  changes: [
2101
2142
  {
2102
2143
  type: 'defaultRedirect',
@@ -3719,6 +3760,21 @@ function splitUrlExperienceVerification(data, expected) {
3719
3760
  if (!experienceIdentityMatches(actual, expected)) {
3720
3761
  return { matches: false, normalizedCodeWhitespace: false };
3721
3762
  }
3763
+ const arms = collectionFrom(actual.variations);
3764
+ const expectedArms = collectionFrom(expected.variations);
3765
+ if (arms.length !== expectedArms.length || validateSplitUrlArms(arms) !== null
3766
+ || new Set(arms.map((arm) => Number(arm.id))).size !== arms.length
3767
+ || arms.some((arm) => !Number.isSafeInteger(Number(arm.id)) || Number(arm.id) <= 0)
3768
+ || arms.some((arm) => (arm.is_baseline === true)
3769
+ !== (expectedArms.find((item) => item.name === arm.name)?.is_baseline === true))) {
3770
+ return { matches: false, normalizedCodeWhitespace: false };
3771
+ }
3772
+ for (const arm of arms) {
3773
+ const expectedArm = expectedArms.find((item) => item.name === arm.name);
3774
+ if (!expectedArm || arm.changes.length !== collectionFrom(expectedArm.changes).length) {
3775
+ return { matches: false, normalizedCodeWhitespace: false };
3776
+ }
3777
+ }
3722
3778
  return expectedVariationsVerification(actual.variations, expected.variations, false);
3723
3779
  }
3724
3780
  function experienceIdentityMatches(actual, expected) {