@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/namespaced.js +29 -2
- package/build/namespaced.js.map +1 -1
- package/build/operator.js +60 -4
- package/build/operator.js.map +1 -1
- package/build/runtime.d.ts +6 -1
- package/build/runtime.js +17 -7
- package/build/runtime.js.map +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
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
|
|
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
|
-
:
|
|
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) {
|