@convertcom/mcp-server 0.0.1-beta.10
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/.assets/logo-dark.svg +20 -0
- package/.assets/logo-light.svg +20 -0
- package/README.md +277 -0
- package/build/api-config.d.ts +4 -0
- package/build/api-config.js +139 -0
- package/build/api-config.js.map +1 -0
- package/build/debug-log.d.ts +6 -0
- package/build/debug-log.js +46 -0
- package/build/debug-log.js.map +1 -0
- package/build/executor.d.ts +13 -0
- package/build/executor.js +783 -0
- package/build/executor.js.map +1 -0
- package/build/index.d.ts +10 -0
- package/build/index.js +453 -0
- package/build/index.js.map +1 -0
- package/build/kb-hybrid.d.ts +20 -0
- package/build/kb-hybrid.js +70 -0
- package/build/kb-hybrid.js.map +1 -0
- package/build/kb-search.d.ts +18 -0
- package/build/kb-search.js +77 -0
- package/build/kb-search.js.map +1 -0
- package/build/kb-semantic.d.ts +20 -0
- package/build/kb-semantic.js +135 -0
- package/build/kb-semantic.js.map +1 -0
- package/build/namespaced.d.ts +10 -0
- package/build/namespaced.js +927 -0
- package/build/namespaced.js.map +1 -0
- package/build/operator.d.ts +17 -0
- package/build/operator.js +1320 -0
- package/build/operator.js.map +1 -0
- package/build/prompts.d.ts +13 -0
- package/build/prompts.js +201 -0
- package/build/prompts.js.map +1 -0
- package/build/shared.d.ts +6 -0
- package/build/shared.js +14 -0
- package/build/shared.js.map +1 -0
- package/build/tools.d.ts +19 -0
- package/build/tools.js +1293 -0
- package/build/tools.js.map +1 -0
- package/data/convert-kb.json +28882 -0
- package/data/kb-vectors/0/embedding.index.3.proxima +0 -0
- package/data/kb-vectors/0/scalar.0.ipc +0 -0
- package/data/kb-vectors/LOCK +0 -0
- package/data/kb-vectors/del.0 +0 -0
- package/data/kb-vectors/idmap.0/000008.sst +0 -0
- package/data/kb-vectors/idmap.0/CURRENT +1 -0
- package/data/kb-vectors/idmap.0/IDENTITY +1 -0
- package/data/kb-vectors/idmap.0/LOCK +0 -0
- package/data/kb-vectors/idmap.0/LOG +0 -0
- package/data/kb-vectors/idmap.0/MANIFEST-000005 +0 -0
- package/data/kb-vectors/idmap.0/OPTIONS-000007 +199 -0
- package/data/kb-vectors/manifest.3 +9 -0
- package/package.json +71 -0
- package/server.json +17 -0
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
import { toolDefinitionMap } from './tools.js';
|
|
2
|
+
import { executeToolByName } from './executor.js';
|
|
3
|
+
import { JSON_SCHEMA_DRAFT_2020_12, pathParamsFromTemplate } from './shared.js';
|
|
4
|
+
function requiredPathParamsForDef(def) {
|
|
5
|
+
const declared = (def.executionParameters || [])
|
|
6
|
+
.filter((p) => p.in === 'path')
|
|
7
|
+
.map((p) => p.name);
|
|
8
|
+
const inferred = def.pathTemplate ? pathParamsFromTemplate(def.pathTemplate) : [];
|
|
9
|
+
return [...new Set([...declared, ...inferred])];
|
|
10
|
+
}
|
|
11
|
+
const REGISTRY = {
|
|
12
|
+
accounts: {
|
|
13
|
+
list: 'getaccountslist',
|
|
14
|
+
get: 'getaccountdetails',
|
|
15
|
+
get_billing_portal: 'getaccountbillingportal',
|
|
16
|
+
get_live_data: 'getaccountlivedata',
|
|
17
|
+
get_history: 'getaccounthistory',
|
|
18
|
+
list_users_accesses: 'getaccountusersaccesseslist',
|
|
19
|
+
list_api_keys: 'getapikeyslist',
|
|
20
|
+
list_sdk_keys: 'getsdkkeyslist',
|
|
21
|
+
create_sdk_key: 'createsdkkey',
|
|
22
|
+
},
|
|
23
|
+
projects: {
|
|
24
|
+
list: 'getprojectslist',
|
|
25
|
+
get: 'getproject',
|
|
26
|
+
create: 'createproject',
|
|
27
|
+
update: 'updateproject',
|
|
28
|
+
get_live_data: 'getprojectlivedata',
|
|
29
|
+
get_history: 'getprojecthistory',
|
|
30
|
+
},
|
|
31
|
+
domains: {
|
|
32
|
+
list: 'getdomainslist',
|
|
33
|
+
get: 'getdomain',
|
|
34
|
+
get_by_url: 'getdomainbyurl',
|
|
35
|
+
check_code: 'checkcodedomain',
|
|
36
|
+
create: 'createdomain',
|
|
37
|
+
update: 'updatedomain',
|
|
38
|
+
delete: 'deletedomain',
|
|
39
|
+
},
|
|
40
|
+
experiences: {
|
|
41
|
+
list: 'getexperienceslist',
|
|
42
|
+
get: 'getexperience',
|
|
43
|
+
get_by_key: 'getexperiencebykey',
|
|
44
|
+
create: 'createexperience',
|
|
45
|
+
clone: 'cloneexperience',
|
|
46
|
+
update: 'updateexperience',
|
|
47
|
+
get_live_data: 'getexperiencelivedata',
|
|
48
|
+
get_history: 'getexperiencehistory',
|
|
49
|
+
aggregated_report: 'getexperienceaggregatedreport',
|
|
50
|
+
daily_report: 'getexperiencedailyreport',
|
|
51
|
+
daily_traffic_allocation: 'getexperiencedailytrafficallocation',
|
|
52
|
+
update_variation: 'updateexperiencevariation',
|
|
53
|
+
get_variation_screenshot: 'getexperiencevariationscreenshot',
|
|
54
|
+
get_heatmap_background: 'getexperienceheatmapbackground',
|
|
55
|
+
get_heatmap_overlay: 'getexperienceheatmapoverlay',
|
|
56
|
+
upload_variation_screenshot: 'uploadexperiencevariationscreenshot',
|
|
57
|
+
convert_variation: 'convertexperiencevariation',
|
|
58
|
+
get_change: 'getexperiencechange',
|
|
59
|
+
update_change: 'updateexperiencechange',
|
|
60
|
+
},
|
|
61
|
+
sections: {
|
|
62
|
+
list: 'getsections',
|
|
63
|
+
create: 'createsection',
|
|
64
|
+
update: 'updatesection',
|
|
65
|
+
update_experience_sections: 'updateexperiencesections',
|
|
66
|
+
add_version: 'addexperiencesectionversion',
|
|
67
|
+
update_version: 'updateexperiencesectionversion',
|
|
68
|
+
},
|
|
69
|
+
goals: {
|
|
70
|
+
list: 'getgoalslist',
|
|
71
|
+
get: 'getgoal',
|
|
72
|
+
get_by_key: 'getgoalbykey',
|
|
73
|
+
create: 'creategoal',
|
|
74
|
+
update: 'updategoal',
|
|
75
|
+
delete: 'deletegoal',
|
|
76
|
+
clone: 'clonegoal',
|
|
77
|
+
},
|
|
78
|
+
tags: {
|
|
79
|
+
list: 'gettagslist',
|
|
80
|
+
get: 'gettag',
|
|
81
|
+
create: 'createtag',
|
|
82
|
+
update: 'updatetag',
|
|
83
|
+
delete: 'deletetag',
|
|
84
|
+
},
|
|
85
|
+
hypotheses: {
|
|
86
|
+
list: 'gethypotheseslist',
|
|
87
|
+
get: 'gethypothesis',
|
|
88
|
+
create: 'createhypothesis',
|
|
89
|
+
update: 'updatehypothesis',
|
|
90
|
+
update_status: 'updatehypothesisstatus',
|
|
91
|
+
convert: 'converthypothesis',
|
|
92
|
+
delete: 'deletehypothesis',
|
|
93
|
+
},
|
|
94
|
+
knowledgebases: {
|
|
95
|
+
list: 'getknowledgebaseslist',
|
|
96
|
+
get: 'getknowledgebase',
|
|
97
|
+
create: 'createknowledgebase',
|
|
98
|
+
update: 'updateknowledgebase',
|
|
99
|
+
delete: 'deleteknowledgebase',
|
|
100
|
+
},
|
|
101
|
+
observations: {
|
|
102
|
+
list: 'getobservationslist',
|
|
103
|
+
get: 'getobservation',
|
|
104
|
+
create: 'createobservation',
|
|
105
|
+
update: 'updateobservation',
|
|
106
|
+
delete: 'deleteobservation',
|
|
107
|
+
},
|
|
108
|
+
locations: {
|
|
109
|
+
list: 'getlocationslist',
|
|
110
|
+
get: 'getlocation',
|
|
111
|
+
create: 'createlocation',
|
|
112
|
+
update: 'updatelocation',
|
|
113
|
+
delete: 'deletelocation',
|
|
114
|
+
presets: 'getlocationspresets',
|
|
115
|
+
clone: 'clonelocation',
|
|
116
|
+
},
|
|
117
|
+
audiences: {
|
|
118
|
+
list: 'getaudienceslist',
|
|
119
|
+
get: 'getaudience',
|
|
120
|
+
create: 'createaudience',
|
|
121
|
+
update: 'updateaudience',
|
|
122
|
+
delete: 'deleteaudience',
|
|
123
|
+
presets: 'getaudiencespresets',
|
|
124
|
+
clone: 'cloneaudience',
|
|
125
|
+
},
|
|
126
|
+
features: {
|
|
127
|
+
list: 'getfeatureslist',
|
|
128
|
+
get: 'getfeature',
|
|
129
|
+
get_by_key: 'getfeaturebykey',
|
|
130
|
+
create: 'createfeature',
|
|
131
|
+
update: 'updatefeature',
|
|
132
|
+
delete: 'deletefeature',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
/** Human-readable titles for each namespace */
|
|
136
|
+
const NAMESPACE_TITLES = {
|
|
137
|
+
accounts: 'Accounts',
|
|
138
|
+
projects: 'Projects',
|
|
139
|
+
domains: 'Domains',
|
|
140
|
+
experiences: 'Experiences',
|
|
141
|
+
sections: 'Sections',
|
|
142
|
+
goals: 'Goals',
|
|
143
|
+
tags: 'Tags',
|
|
144
|
+
hypotheses: 'Hypotheses',
|
|
145
|
+
knowledgebases: 'Knowledge Bases',
|
|
146
|
+
observations: 'Observations',
|
|
147
|
+
locations: 'Locations',
|
|
148
|
+
audiences: 'Audiences',
|
|
149
|
+
features: 'Features',
|
|
150
|
+
};
|
|
151
|
+
/** Namespace-level descriptions for agents — richer than the auto-generated "Namespace 'X'" */
|
|
152
|
+
const NAMESPACE_DESCRIPTIONS = {
|
|
153
|
+
accounts: 'Manage Convert.com accounts via the live API — get account details, billing portal URLs, live visitor data, history, users, API/SDK keys.',
|
|
154
|
+
projects: 'Manage Convert.com projects via the live API — list, create, update projects, get live visitor data and change history.',
|
|
155
|
+
domains: 'Manage domains in the user\'s Convert.com account — add, verify (check_code), update, delete domains for a project.',
|
|
156
|
+
experiences: 'Manage A/B tests, MVTs, split URL tests, deploys, feature rollouts, reports, screenshots, and heatmaps in the user\'s Convert.com account. Use type "a/b" (with slash, not underscore).',
|
|
157
|
+
sections: 'Manage multi-page sections within MVT experiences via the Convert.com API. Requires experience_id in path — sections are experience-scoped.',
|
|
158
|
+
goals: 'Manage conversion goals in the user\'s Convert.com account — page visits, clicks, form submits, revenue, custom JS. Use goal_key for get_by_key.',
|
|
159
|
+
tags: 'Manage tags in the user\'s Convert.com account for organizing experiences and other resources.',
|
|
160
|
+
hypotheses: 'Manage CRO hypotheses in the user\'s Convert.com account with PIE/ICE prioritization scoring (UPPERCASE).',
|
|
161
|
+
knowledgebases: 'Manage knowledge base entries in the user\'s Convert.com account. Note: path param uses underscores — knowledge_base_id (not knowledgebase_id).',
|
|
162
|
+
observations: 'Manage observations in the user\'s Convert.com account. ⚠️ update action has a known backend bug (500).',
|
|
163
|
+
locations: 'Manage page targeting rules in the user\'s Convert.com account (URL match, JS condition, DOM element trigger). Rules use nested OR→AND→OR_WHEN structure.',
|
|
164
|
+
audiences: 'Manage visitor segmentation rules in the user\'s Convert.com account. Rules use nested OR→AND→OR_WHEN structure similar to locations.',
|
|
165
|
+
features: 'Manage feature flags in the user\'s Convert.com account — only works in fullstack project types. Returns 422 for web/standard projects.',
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Per-action agent guidance. Appended to generated descriptions as "Agent Notes:".
|
|
169
|
+
* Keys are "namespace.action" strings matching REGISTRY entries.
|
|
170
|
+
* This map survives `yarn generate` — it's hand-curated like REGISTRY.
|
|
171
|
+
*/
|
|
172
|
+
export const ACTION_NOTES = {
|
|
173
|
+
// ── accounts ─────────────────────────────────────────────
|
|
174
|
+
'accounts.get_billing_portal': 'Returns an authenticated Paddle billing portal URL for subscription and invoice management. Use only when the user asks for billing, subscription, or invoice access; do not surface billing links proactively.',
|
|
175
|
+
// ── experiences ──────────────────────────────────────────
|
|
176
|
+
'experiences.create': [
|
|
177
|
+
'Type enum: "a/b", "a/a", "mvt", "split_url", "multipage", "deploy" (standard projects), "a/b_fullstack", "feature_rollout" (fullstack projects only).',
|
|
178
|
+
'Required body fields: name, type, url. If status is omitted, MCP defaults it to "draft".',
|
|
179
|
+
'Setting status to "active" requires explicit user confirmation at tool-call level: confirmation: { "user_confirmed": true, "reason": "..." }.',
|
|
180
|
+
'For hand-written JS/CSS in an A/B test, use changes[].type "customCode" with data { "js": "...", "css": "" }. Do not use "defaultCode" unless sending Visual Editor data with custom_js.',
|
|
181
|
+
'Create control+treatment variations by sending body.variations with each variation name and optional changes array. Use traffic_distribution for variation traffic; MCP also accepts legacy traffic_percentage and normalizes it.',
|
|
182
|
+
'a/b_fullstack and feature_rollout return 422 on non-fullstack projects.',
|
|
183
|
+
'Custom-code example: { "body": { "name": "My Test", "type": "a/b", "status": "draft", "url": "https://example.com", "variations": [{ "name": "control", "changes": [{ "type": "customCode", "data": { "js": "", "css": "" } }] }, { "name": "Treatment", "changes": [{ "type": "customCode", "data": { "js": "console.log(1)", "css": "" } }] }] } }',
|
|
184
|
+
].join('\n'),
|
|
185
|
+
'experiences.update': [
|
|
186
|
+
'Setting body.status to "active" requires explicit user confirmation at tool-call level: confirmation: { "user_confirmed": true, "reason": "..." }.',
|
|
187
|
+
'Experience-level settings belong here, not in update_variation. For split URL regex support, call experiences.update without variation_id: { "body": { "settings": { "split_url_settings": { "split_regex_support": true } } } }.',
|
|
188
|
+
'MCP accepts legacy settings arrays with one object and normalizes them to the canonical settings object.',
|
|
189
|
+
].join('\n'),
|
|
190
|
+
'experiences.get_by_key': 'Path parameter is "experience_key" (not "key"). Pass in path: { "experience_key": "abc123" }.',
|
|
191
|
+
'experiences.daily_traffic_allocation': 'Only works when MAB (Multi-Armed Bandit) is configured on the experience. Returns 422 otherwise.',
|
|
192
|
+
'experiences.update_variation': [
|
|
193
|
+
'Body is required and must be a non-empty object. Empty/missing body is rejected at MCP layer to avoid silent no-op updates.',
|
|
194
|
+
'Requires "variation_id" in path. Get variation IDs from experiences.get with query: { "include": ["variations"], "expand": ["variations"] }.',
|
|
195
|
+
'To add CSS/JS changes, use the "changes" array in body. Each change needs "type" and "data":',
|
|
196
|
+
' - type "customCode": data = { css: "...", js: "..." } — for custom CSS/JS code',
|
|
197
|
+
' - type "defaultCode": data = { css: "...", js: "...", custom_js: "..." } — for Visual Editor changes',
|
|
198
|
+
' - type "defaultRedirect": data = { original_pattern, variation_pattern, case_sensitive } — for split URL',
|
|
199
|
+
' - type "fullStackFeature": data = { feature_id, variables_data } — for fullstack features',
|
|
200
|
+
'Do NOT send experience-level settings here. For split_url_settings such as split_regex_support, use experiences.update and omit variation_id.',
|
|
201
|
+
'Example adding custom CSS+JS: { "body": { "changes": [{ "type": "customCode", "data": { "css": "body{color:red}", "js": "console.log(1)" } }] } }',
|
|
202
|
+
'To clear all changes: { "body": { "changes": [] } }',
|
|
203
|
+
'To update an existing change, include its integer "id" from the previous response.',
|
|
204
|
+
].join('\n'),
|
|
205
|
+
'experiences.get_variation_screenshot': 'Requires "variation_id" in path. Returns 404 ("No image found") for draft experiences or variations with no visual editor changes yet — this is normal.',
|
|
206
|
+
'experiences.upload_variation_screenshot': 'Requires "variation_id" in path and multipart body { "image": "data:image/png;base64,..." }. MCP also accepts a raw data URL string as body and normalizes it to the image field.',
|
|
207
|
+
'experiences.get_change': [
|
|
208
|
+
'Requires account_id, project_id, experience_id, and change_id in path.',
|
|
209
|
+
'⚠️ Known schema issue: path params may not be auto-validated. Pass all four IDs explicitly in path.',
|
|
210
|
+
'Get change IDs from the update_variation response or from experiences.get with include/expand variations.',
|
|
211
|
+
].join('\n'),
|
|
212
|
+
'experiences.update_change': [
|
|
213
|
+
'Requires account_id, project_id, experience_id, and change_id in path.',
|
|
214
|
+
'⚠️ Known schema issue: path params may not be auto-validated. Pass all four IDs explicitly in path.',
|
|
215
|
+
'Body uses same structure as update_variation changes: { type, data }.',
|
|
216
|
+
'For customCode: { "type": "customCode", "data": { "css": "...", "js": "..." } }',
|
|
217
|
+
'For defaultCode: { "type": "defaultCode", "data": { "css": "...", "js": "...", "custom_js": "..." } }',
|
|
218
|
+
].join('\n'),
|
|
219
|
+
'experiences.aggregated_report': 'Works on any experience. Returns empty data arrays for draft/inactive experiences.',
|
|
220
|
+
'experiences.daily_report': 'Works on any experience. Returns empty data arrays for draft/inactive experiences.',
|
|
221
|
+
'experiences.get_heatmap_background': 'Requires heatmap_id in path. Optional body.device is one of "desktop", "tablet", or "mobile" and defaults to "desktop".',
|
|
222
|
+
'experiences.get_heatmap_overlay': 'Requires heatmap_id in path. Optional body.device is one of "desktop", "tablet", or "mobile"; optional body.interaction is "click", "scroll", or "move". Defaults are desktop/click.',
|
|
223
|
+
// ── locations ────────────────────────────────────────────
|
|
224
|
+
'locations.create': [
|
|
225
|
+
'Required body fields: name, rules, trigger.',
|
|
226
|
+
'Rules use nested OR→AND→OR_WHEN structure.',
|
|
227
|
+
'Minimal example: { "body": { "name": "Homepage", "rules": { "OR": [{ "AND": [{ "OR_WHEN": [{ "rule_type": "url", "value": "example.com", "matching": { "match_type": "contains" } }] }] }] }, "trigger": { "type": "upon_run" } } }',
|
|
228
|
+
].join('\n'),
|
|
229
|
+
// ── audiences ────────────────────────────────────────────
|
|
230
|
+
'audiences.create': [
|
|
231
|
+
'Required body fields: name, type, rules.',
|
|
232
|
+
'Type enum: "segmentation", "permanent", "transient". MCP accepts legacy "transient_condition" and normalizes it to "transient".',
|
|
233
|
+
'Rules use nested OR→AND→OR_WHEN structure (same as locations but more rule_type options).',
|
|
234
|
+
'For visitor_type rules, use match_type: "equals" (not "matches").',
|
|
235
|
+
'Minimal example: { "body": { "name": "Returning Visitors", "type": "segmentation", "rules": { "OR": [{ "AND": [{ "OR_WHEN": [{ "rule_type": "visitor_type", "value": "returning", "matching": { "match_type": "equals" } }] }] }] } } }',
|
|
236
|
+
].join('\n'),
|
|
237
|
+
// ── hypotheses ───────────────────────────────────────────
|
|
238
|
+
'hypotheses.create': [
|
|
239
|
+
'Required body fields: name, status (use "draft"), prioritization_score_type, prioritization_score_attributes.',
|
|
240
|
+
'prioritization_score_type: "PIE" or "ICE" (UPPERCASE only).',
|
|
241
|
+
'For PIE: attributes are { potential, importance, ease } (values 1-5).',
|
|
242
|
+
'For ICE: attributes are { impact, confidence, ease } (values 1-5).',
|
|
243
|
+
'Minimal example: { "body": { "name": "Bigger CTA button", "status": "draft", "prioritization_score_type": "PIE", "prioritization_score_attributes": { "potential": 4, "importance": 3, "ease": 5 } } }',
|
|
244
|
+
].join('\n'),
|
|
245
|
+
'hypotheses.convert': 'Converts hypothesis to experience. The hypothesis must have associated goals and locations first. Returns 422 if prerequisites are missing.',
|
|
246
|
+
'hypotheses.update_status': [
|
|
247
|
+
'Body is { "status": "<value>" }. Valid statuses: "draft", "applied", "completed", "archived", "proven", "disproven".',
|
|
248
|
+
'Status transitions follow a strict state machine:',
|
|
249
|
+
' draft ↔ applied → completed → { archived, proven, disproven }',
|
|
250
|
+
'Only allowed transitions: draft→applied, applied→draft, applied→completed, completed→archived, completed→proven, completed→disproven.',
|
|
251
|
+
'Returns 422 "Status cannot be updated to the desired one" for invalid transitions.',
|
|
252
|
+
].join('\n'),
|
|
253
|
+
// ── knowledgebases ───────────────────────────────────────
|
|
254
|
+
'knowledgebases.create': 'Required body fields: name, summary. Optional: description, url, start_date, end_date.',
|
|
255
|
+
'knowledgebases.get': 'Path parameter is "knowledge_base_id" (with underscores, not "knowledgebase_id").',
|
|
256
|
+
'knowledgebases.update': [
|
|
257
|
+
'Path parameter is "knowledge_base_id" (with underscores).',
|
|
258
|
+
'Currently only accepts "status" field in body ("active" or "archived"). Other fields like name or summary will fail schema validation.',
|
|
259
|
+
].join('\n'),
|
|
260
|
+
'knowledgebases.delete': 'Path parameter is "knowledge_base_id" (with underscores, not "knowledgebase_id").',
|
|
261
|
+
// ── observations ─────────────────────────────────────────
|
|
262
|
+
'observations.update': '⚠️ Known backend bug — API returns 500 on update. Workaround: create a new observation + delete the old one.',
|
|
263
|
+
// ── features ─────────────────────────────────────────────
|
|
264
|
+
'features.create': [
|
|
265
|
+
'Only works in fullstack project types. Returns 422 for web/standard projects.',
|
|
266
|
+
'Required body fields: name, key, variables (array).',
|
|
267
|
+
'Variable type enum: "boolean", "string", "integer", "float", "json". default_value must match the type (e.g., true for boolean, not "true").',
|
|
268
|
+
'Minimal example: { "body": { "name": "Dark Mode", "key": "dark_mode", "variables": [{ "key": "enabled", "type": "boolean", "default_value": false }] } }',
|
|
269
|
+
].join('\n'),
|
|
270
|
+
'features.get_by_key': 'Path parameter is "feature_key" (not "key"). Pass in path: { "feature_key": "abc123" }.',
|
|
271
|
+
// ── sections ─────────────────────────────────────────────
|
|
272
|
+
'sections.list': 'Requires "experience_id" in path — sections are experience-scoped, not project-scoped.',
|
|
273
|
+
'sections.create': [
|
|
274
|
+
'Requires "experience_id" in path — sections are experience-scoped.',
|
|
275
|
+
'Only works for MVT (multivariate) experiences. Returns 403 for A/B and other types.',
|
|
276
|
+
'Required body field: "versions" array with at least one version object.',
|
|
277
|
+
'Minimal example: { "body": { "name": "New Section", "versions": [{ "name": "Original", "url": "https://example.com" }] } }',
|
|
278
|
+
].join('\n'),
|
|
279
|
+
'sections.update': [
|
|
280
|
+
'Requires "experience_id" and "section_id" in path — sections are experience-scoped.',
|
|
281
|
+
'Required body field: "versions" array (same format as sections.create).',
|
|
282
|
+
'Example: { "body": { "name": "Updated Section", "versions": [{ "name": "V1", "url": "https://example.com" }] } }',
|
|
283
|
+
].join('\n'),
|
|
284
|
+
// ── goals ────────────────────────────────────────────────
|
|
285
|
+
'goals.get_by_key': 'Path parameter is "goal_key" (not "key"). Pass in path: { "goal_key": "abc123" }.',
|
|
286
|
+
'goals.create': [
|
|
287
|
+
'Type enum: "advanced", "visits_page", "revenue", "clicks_link", "submits_form", "clicks_element", "dom_interaction", "scroll_percentage", "ga_import", "code_trigger".',
|
|
288
|
+
'MCP accepts legacy type aliases "custom_js" -> "code_trigger" and "scrolls_to" -> "scroll_percentage".',
|
|
289
|
+
'description max length is 500 characters.',
|
|
290
|
+
'Use triggering_rule for URL or audience-style scoping; it accepts nested OR→AND→OR_WHEN rules.',
|
|
291
|
+
'clicks_element settings example: { "selector": ".cta" }. code_trigger requires no settings.',
|
|
292
|
+
'clicks_link requires settings.href as a string; Convert API rejects match objects there. Use triggering_rule for page scoping, or create multiple link goals when multiple href patterns are needed.',
|
|
293
|
+
].join('\n'),
|
|
294
|
+
// ── accounts ─────────────────────────────────────────────
|
|
295
|
+
'accounts.list_sdk_keys': 'Requires "project_id" in path even though it\'s under the accounts namespace.',
|
|
296
|
+
'accounts.create_sdk_key': 'Requires "project_id" in path even though it\'s under the accounts namespace. May return 401 if API credentials lack SDK key management permissions.',
|
|
297
|
+
};
|
|
298
|
+
function isNonArrayObject(value) {
|
|
299
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
300
|
+
}
|
|
301
|
+
function normalizeLooseJson(input) {
|
|
302
|
+
return input
|
|
303
|
+
.replace(/[`]/g, '"')
|
|
304
|
+
.replace(/[\u201C\u201D]/g, '"')
|
|
305
|
+
.replace(/[\u2018\u2019]/g, "'")
|
|
306
|
+
.replace(/(\{|\,)(\s*)([A-Za-z0-9_]+)(\s*):/g, (_, start, space, key, after) => `${start}${space}"${key}"${after}:`)
|
|
307
|
+
.replace(/:\s*'([^']*)'/g, ': "$1"')
|
|
308
|
+
.replace(/,\s*([}\]])/g, '$1');
|
|
309
|
+
}
|
|
310
|
+
function tryParseJsonLike(value) {
|
|
311
|
+
if (!value)
|
|
312
|
+
return undefined;
|
|
313
|
+
const attempts = [value, normalizeLooseJson(value)];
|
|
314
|
+
for (const candidate of attempts) {
|
|
315
|
+
try {
|
|
316
|
+
return JSON.parse(candidate);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
function coerceJsonLike(value) {
|
|
325
|
+
if (typeof value !== 'string')
|
|
326
|
+
return value;
|
|
327
|
+
const parsed = tryParseJsonLike(value);
|
|
328
|
+
return typeof parsed === 'undefined' ? value : parsed;
|
|
329
|
+
}
|
|
330
|
+
function asObject(value) {
|
|
331
|
+
return isNonArrayObject(value) ? value : null;
|
|
332
|
+
}
|
|
333
|
+
function coerceObject(value) {
|
|
334
|
+
const parsed = coerceJsonLike(value);
|
|
335
|
+
const objectValue = asObject(parsed);
|
|
336
|
+
return objectValue ? { ...objectValue } : {};
|
|
337
|
+
}
|
|
338
|
+
function coerceParameterValue(value, schema) {
|
|
339
|
+
if (typeof value !== 'string')
|
|
340
|
+
return value;
|
|
341
|
+
const trimmed = value.trim();
|
|
342
|
+
if (!trimmed)
|
|
343
|
+
return value;
|
|
344
|
+
switch (schema?.type) {
|
|
345
|
+
case 'integer': {
|
|
346
|
+
if (!/^-?\d+$/.test(trimmed))
|
|
347
|
+
return value;
|
|
348
|
+
const parsed = Number(trimmed);
|
|
349
|
+
return Number.isSafeInteger(parsed) ? parsed : value;
|
|
350
|
+
}
|
|
351
|
+
case 'number': {
|
|
352
|
+
if (!/^-?\d+(?:\.\d+)?$/.test(trimmed))
|
|
353
|
+
return value;
|
|
354
|
+
const parsed = Number(trimmed);
|
|
355
|
+
return Number.isFinite(parsed) ? parsed : value;
|
|
356
|
+
}
|
|
357
|
+
case 'boolean':
|
|
358
|
+
if (trimmed === 'true')
|
|
359
|
+
return true;
|
|
360
|
+
if (trimmed === 'false')
|
|
361
|
+
return false;
|
|
362
|
+
return value;
|
|
363
|
+
case 'array': {
|
|
364
|
+
const parsed = tryParseJsonLike(trimmed);
|
|
365
|
+
return Array.isArray(parsed) ? parsed : value;
|
|
366
|
+
}
|
|
367
|
+
case 'object': {
|
|
368
|
+
const parsed = tryParseJsonLike(trimmed);
|
|
369
|
+
return isNonArrayObject(parsed) ? parsed : value;
|
|
370
|
+
}
|
|
371
|
+
default:
|
|
372
|
+
return value;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function normalizeNamespaceArgs(rawArgs, def) {
|
|
376
|
+
const path = coerceObject(rawArgs.path);
|
|
377
|
+
const query = coerceObject(rawArgs.query);
|
|
378
|
+
const pathParamNames = requiredPathParamsForDef(def);
|
|
379
|
+
const executionParams = def.executionParameters || [];
|
|
380
|
+
for (const name of pathParamNames) {
|
|
381
|
+
const param = executionParams.find((candidate) => candidate.in === 'path' && candidate.name === name);
|
|
382
|
+
if (typeof path[name] !== 'undefined') {
|
|
383
|
+
path[name] = coerceParameterValue(path[name], param?.schema);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (typeof rawArgs[name] !== 'undefined') {
|
|
387
|
+
path[name] = coerceParameterValue(rawArgs[name], param?.schema);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const param of executionParams) {
|
|
391
|
+
if (param.in !== 'query' && param.in !== 'header')
|
|
392
|
+
continue;
|
|
393
|
+
if (typeof query[param.name] !== 'undefined') {
|
|
394
|
+
query[param.name] = coerceParameterValue(query[param.name], param.schema);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (typeof rawArgs[param.name] !== 'undefined') {
|
|
398
|
+
query[param.name] = coerceParameterValue(rawArgs[param.name], param.schema);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
defaultExpandFromInclude(query, executionParams);
|
|
402
|
+
const bodySource = typeof rawArgs.body !== 'undefined' ? rawArgs.body : rawArgs.requestBody;
|
|
403
|
+
const body = coerceJsonLike(bodySource);
|
|
404
|
+
const confirmation = coerceJsonLike(rawArgs.confirmation);
|
|
405
|
+
return {
|
|
406
|
+
action: String(rawArgs.action),
|
|
407
|
+
path,
|
|
408
|
+
query,
|
|
409
|
+
body,
|
|
410
|
+
confirmation,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function hasActivationConfirmation(confirmation) {
|
|
414
|
+
if (!isNonArrayObject(confirmation))
|
|
415
|
+
return false;
|
|
416
|
+
return confirmation.user_confirmed === true;
|
|
417
|
+
}
|
|
418
|
+
function normalizeBodyForAction(namespace, action, body) {
|
|
419
|
+
if (namespace === 'experiences' && action === 'upload_variation_screenshot' && typeof body === 'string') {
|
|
420
|
+
return { image: body };
|
|
421
|
+
}
|
|
422
|
+
if (!isNonArrayObject(body))
|
|
423
|
+
return body;
|
|
424
|
+
if (namespace === 'audiences')
|
|
425
|
+
return normalizeAudienceBody(body);
|
|
426
|
+
if (namespace === 'goals')
|
|
427
|
+
return normalizeGoalBody(body);
|
|
428
|
+
if (namespace !== 'experiences')
|
|
429
|
+
return body;
|
|
430
|
+
return normalizeExperienceBody(action, body);
|
|
431
|
+
}
|
|
432
|
+
function normalizeExperienceBody(action, body) {
|
|
433
|
+
const normalized = { ...body };
|
|
434
|
+
if ((action === 'create' || action === 'update') && Array.isArray(normalized.settings)) {
|
|
435
|
+
const [onlySettings] = normalized.settings;
|
|
436
|
+
if (normalized.settings.length === 1 && isNonArrayObject(onlySettings)) {
|
|
437
|
+
normalized.settings = { ...onlySettings };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if ((action === 'create' || action === 'update') && Array.isArray(normalized.variations)) {
|
|
441
|
+
normalized.variations = normalizeVariationList(normalized.variations);
|
|
442
|
+
}
|
|
443
|
+
else if (action === 'update_variation') {
|
|
444
|
+
return normalizeVariationTrafficAlias(normalized);
|
|
445
|
+
}
|
|
446
|
+
if (action !== 'create')
|
|
447
|
+
return normalized;
|
|
448
|
+
const statusRaw = body.status;
|
|
449
|
+
const isMissing = statusRaw === undefined || statusRaw === null;
|
|
450
|
+
const isEmpty = typeof statusRaw === 'string' && statusRaw.trim() === '';
|
|
451
|
+
if (isMissing || isEmpty) {
|
|
452
|
+
normalized.status = 'draft';
|
|
453
|
+
}
|
|
454
|
+
return normalized;
|
|
455
|
+
}
|
|
456
|
+
function normalizeAudienceBody(body) {
|
|
457
|
+
const normalized = { ...body };
|
|
458
|
+
if (normalized.type === 'transient_condition') {
|
|
459
|
+
normalized.type = 'transient';
|
|
460
|
+
}
|
|
461
|
+
return normalized;
|
|
462
|
+
}
|
|
463
|
+
function normalizeGoalBody(body) {
|
|
464
|
+
const normalized = { ...body };
|
|
465
|
+
if (normalized.type === 'custom_js') {
|
|
466
|
+
normalized.type = 'code_trigger';
|
|
467
|
+
}
|
|
468
|
+
else if (normalized.type === 'scrolls_to') {
|
|
469
|
+
normalized.type = 'scroll_percentage';
|
|
470
|
+
}
|
|
471
|
+
return normalized;
|
|
472
|
+
}
|
|
473
|
+
function defaultExpandFromInclude(query, executionParams) {
|
|
474
|
+
if (typeof query.include === 'undefined' || typeof query.expand !== 'undefined')
|
|
475
|
+
return;
|
|
476
|
+
const expandParam = executionParams.find((param) => param.in === 'query' && param.name === 'expand');
|
|
477
|
+
if (!expandParam || !Array.isArray(query.include))
|
|
478
|
+
return;
|
|
479
|
+
const includeValues = query.include.filter((value) => typeof value === 'string');
|
|
480
|
+
const allowedExpandValues = expandParam.schema?.items?.enum;
|
|
481
|
+
const expandableValues = Array.isArray(allowedExpandValues) && allowedExpandValues.length > 0
|
|
482
|
+
? includeValues.filter((value) => allowedExpandValues.includes(value))
|
|
483
|
+
: includeValues;
|
|
484
|
+
if (expandableValues.length > 0) {
|
|
485
|
+
query.expand = expandableValues;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function normalizeVariationList(value) {
|
|
489
|
+
if (!Array.isArray(value))
|
|
490
|
+
return value;
|
|
491
|
+
return value.map((variation) => {
|
|
492
|
+
return isNonArrayObject(variation) ? normalizeVariationTrafficAlias(variation) : variation;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function normalizeVariationTrafficAlias(variation) {
|
|
496
|
+
if (!Object.prototype.hasOwnProperty.call(variation, 'traffic_percentage')) {
|
|
497
|
+
return variation;
|
|
498
|
+
}
|
|
499
|
+
const normalized = { ...variation };
|
|
500
|
+
if (!Object.prototype.hasOwnProperty.call(normalized, 'traffic_distribution')) {
|
|
501
|
+
normalized.traffic_distribution = normalized.traffic_percentage;
|
|
502
|
+
}
|
|
503
|
+
delete normalized.traffic_percentage;
|
|
504
|
+
return normalized;
|
|
505
|
+
}
|
|
506
|
+
function evaluateGuardrails(namespace, action, body, confirmation, path) {
|
|
507
|
+
if (namespace === 'goals') {
|
|
508
|
+
return evaluateGoalGuardrails(action, body);
|
|
509
|
+
}
|
|
510
|
+
if (namespace !== 'experiences')
|
|
511
|
+
return null;
|
|
512
|
+
if (action === 'update_variation') {
|
|
513
|
+
if (!isNonArrayObject(body) || Object.keys(body).length === 0) {
|
|
514
|
+
return {
|
|
515
|
+
code: 'BodyRequired',
|
|
516
|
+
message: 'experiences.update_variation requires a non-empty "body" object (for example {"changes":[...]} or {"status":"running"}).',
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
if (hasExperienceLevelSettings(body)) {
|
|
520
|
+
const suggestedCall = buildExperienceSettingsUpdateCall(path, body);
|
|
521
|
+
return {
|
|
522
|
+
code: 'WrongActionForExperienceSettings',
|
|
523
|
+
message: `Experience-level settings such as split_url_settings cannot be applied with experiences.update_variation. Retry with experiences.update and omit variation_id:\n${JSON.stringify(suggestedCall, null, 2)}`,
|
|
524
|
+
suggested_call: suggestedCall,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if ((action === 'create' || action === 'update') && isNonArrayObject(body)) {
|
|
529
|
+
if (Object.prototype.hasOwnProperty.call(body, 'site_area')) {
|
|
530
|
+
const suggestedCall = buildLocationCreateCallFromSiteArea(path, body);
|
|
531
|
+
return {
|
|
532
|
+
code: 'UnsupportedInlineSiteArea',
|
|
533
|
+
message: [
|
|
534
|
+
'body.site_area is rejected by Convert MCP because the Convert API can return success while persisting site_area:null.',
|
|
535
|
+
'Create or reuse a Location, then pass its ID in body.locations instead. This prevents silent targeting failures.',
|
|
536
|
+
`Suggested locations.create call:\n${JSON.stringify(suggestedCall, null, 2)}`,
|
|
537
|
+
].join('\n'),
|
|
538
|
+
suggested_call: suggestedCall,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const statusRaw = body.status;
|
|
542
|
+
const status = typeof statusRaw === 'string' ? statusRaw.trim().toLowerCase() : null;
|
|
543
|
+
if (status === 'active' && !hasActivationConfirmation(confirmation)) {
|
|
544
|
+
return {
|
|
545
|
+
code: 'ActivationConfirmationRequired',
|
|
546
|
+
message: 'Setting experience status to "active" requires explicit user confirmation. Ask the user to confirm first, then retry with confirmation: { "user_confirmed": true, "reason": "..." }.',
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
function evaluateGoalGuardrails(action, body) {
|
|
553
|
+
if ((action !== 'create' && action !== 'update') || !isNonArrayObject(body))
|
|
554
|
+
return null;
|
|
555
|
+
if (body.type !== 'clicks_link' || !isNonArrayObject(body.settings))
|
|
556
|
+
return null;
|
|
557
|
+
const href = body.settings.href;
|
|
558
|
+
if (typeof href === 'undefined' || href === null || typeof href === 'string')
|
|
559
|
+
return null;
|
|
560
|
+
return {
|
|
561
|
+
code: 'ClicksLinkHrefMustBeString',
|
|
562
|
+
message: [
|
|
563
|
+
'goals.create/update for type "clicks_link" requires body.settings.href to be a string.',
|
|
564
|
+
'Convert API rejects match objects for settings.href. Use a string href, add URL scoping in triggering_rule, or create separate link goals for separate href patterns.',
|
|
565
|
+
].join('\n'),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function hasExperienceLevelSettings(body) {
|
|
569
|
+
if (Object.prototype.hasOwnProperty.call(body, 'settings'))
|
|
570
|
+
return true;
|
|
571
|
+
if (Object.prototype.hasOwnProperty.call(body, 'split_url_settings'))
|
|
572
|
+
return true;
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
function buildExperienceSettingsUpdateCall(path, body) {
|
|
576
|
+
const settings = normalizeSettingsForSuggestion(body);
|
|
577
|
+
return {
|
|
578
|
+
action: 'update',
|
|
579
|
+
path: {
|
|
580
|
+
account_id: path.account_id ?? '<account_id>',
|
|
581
|
+
project_id: path.project_id ?? '<project_id>',
|
|
582
|
+
experience_id: path.experience_id ?? '<experience_id>',
|
|
583
|
+
},
|
|
584
|
+
body: { settings },
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function normalizeSettingsForSuggestion(body) {
|
|
588
|
+
const rawSettings = body.settings ?? (Object.prototype.hasOwnProperty.call(body, 'split_url_settings')
|
|
589
|
+
? { split_url_settings: body.split_url_settings }
|
|
590
|
+
: {});
|
|
591
|
+
if (Array.isArray(rawSettings) && rawSettings.length === 1 && isNonArrayObject(rawSettings[0])) {
|
|
592
|
+
return rawSettings[0];
|
|
593
|
+
}
|
|
594
|
+
return rawSettings;
|
|
595
|
+
}
|
|
596
|
+
function buildLocationCreateCallFromSiteArea(path, body) {
|
|
597
|
+
const siteArea = isNonArrayObject(body.site_area) ? body.site_area : {};
|
|
598
|
+
const include = Array.isArray(siteArea.include) ? siteArea.include : [];
|
|
599
|
+
const rules = include.length > 0
|
|
600
|
+
? { OR: [{ AND: [{ OR_WHEN: include }] }] }
|
|
601
|
+
: { OR: [{ AND: [{ OR_WHEN: [] }] }] };
|
|
602
|
+
const baseName = typeof body.name === 'string' && body.name.trim() ? body.name.trim() : 'Experience';
|
|
603
|
+
return {
|
|
604
|
+
namespace: 'locations',
|
|
605
|
+
action: 'create',
|
|
606
|
+
path: {
|
|
607
|
+
account_id: path.account_id ?? '<account_id>',
|
|
608
|
+
project_id: path.project_id ?? '<project_id>',
|
|
609
|
+
},
|
|
610
|
+
body: {
|
|
611
|
+
name: `${baseName} Location`,
|
|
612
|
+
rules,
|
|
613
|
+
trigger: { type: 'upon_run' },
|
|
614
|
+
},
|
|
615
|
+
retry_experience_with: {
|
|
616
|
+
locations: ['<created_location_id>'],
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Walk a JSON Schema tree and extract all enum fields with their paths
|
|
622
|
+
* and required fields. Handles allOf/oneOf/anyOf composition.
|
|
623
|
+
* Used to surface schema constraints that LLMs can't parse from raw JSON Schema.
|
|
624
|
+
*/
|
|
625
|
+
function extractBodyHints(inputSchema) {
|
|
626
|
+
const rbSchema = inputSchema?.properties?.requestBody;
|
|
627
|
+
if (!rbSchema)
|
|
628
|
+
return null;
|
|
629
|
+
const visited = new WeakSet();
|
|
630
|
+
const enumMap = new Map();
|
|
631
|
+
const requiredAtLevel = new Map();
|
|
632
|
+
function walk(schema, path) {
|
|
633
|
+
if (!schema || typeof schema !== 'object')
|
|
634
|
+
return;
|
|
635
|
+
if (visited.has(schema))
|
|
636
|
+
return;
|
|
637
|
+
visited.add(schema);
|
|
638
|
+
// Collect enum values
|
|
639
|
+
if (Array.isArray(schema.enum)) {
|
|
640
|
+
const vals = schema.enum.filter((v) => typeof v === 'string' || typeof v === 'number');
|
|
641
|
+
if (vals.length > 0 && path) {
|
|
642
|
+
const existing = enumMap.get(path) || new Set();
|
|
643
|
+
vals.forEach((v) => existing.add(String(v)));
|
|
644
|
+
enumMap.set(path, existing);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
// Collect required fields
|
|
648
|
+
if (Array.isArray(schema.required) && schema.required.length > 0) {
|
|
649
|
+
const level = path || '(root)';
|
|
650
|
+
const existing = requiredAtLevel.get(level) || new Set();
|
|
651
|
+
schema.required.forEach((r) => existing.add(r));
|
|
652
|
+
requiredAtLevel.set(level, existing);
|
|
653
|
+
}
|
|
654
|
+
// Walk properties
|
|
655
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
656
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
657
|
+
const prop = value;
|
|
658
|
+
if (prop.readOnly)
|
|
659
|
+
continue; // skip auto-generated IDs
|
|
660
|
+
walk(prop, path ? `${path}.${key}` : key);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Walk array items
|
|
664
|
+
if (schema.items) {
|
|
665
|
+
walk(schema.items, `${path}[]`);
|
|
666
|
+
}
|
|
667
|
+
// Walk composition keywords
|
|
668
|
+
for (const kw of ['allOf', 'oneOf', 'anyOf']) {
|
|
669
|
+
if (Array.isArray(schema[kw])) {
|
|
670
|
+
for (const sub of schema[kw]) {
|
|
671
|
+
walk(sub, path);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
walk(rbSchema, '');
|
|
677
|
+
const lines = [];
|
|
678
|
+
// Show enum fields with valid values (sorted by path depth, shallow first)
|
|
679
|
+
const enumEntries = [...enumMap.entries()]
|
|
680
|
+
.sort((a, b) => a[0].split('.').length - b[0].split('.').length);
|
|
681
|
+
for (const [field, values] of enumEntries) {
|
|
682
|
+
const valArr = [...values];
|
|
683
|
+
if (valArr.length > 12) {
|
|
684
|
+
lines.push(` ${field}: ${valArr.slice(0, 10).map(v => `"${v}"`).join(', ')}, ... (${valArr.length} values)`);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
lines.push(` ${field}: ${valArr.map(v => `"${v}"`).join(', ')}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// Show required fields per level
|
|
691
|
+
for (const [level, fields] of requiredAtLevel) {
|
|
692
|
+
const fieldArr = [...fields].filter(f => f !== 'id'); // skip auto-assigned ids
|
|
693
|
+
if (fieldArr.length === 0)
|
|
694
|
+
continue;
|
|
695
|
+
if (level === '(root)') {
|
|
696
|
+
lines.push(` Required: ${fieldArr.join(', ')}`);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
lines.push(` Required in ${level}: ${fieldArr.join(', ')}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return lines.length > 0 ? lines.join('\n') : null;
|
|
703
|
+
}
|
|
704
|
+
function actionsForToolset(namespace, toolSet) {
|
|
705
|
+
const all = Object.entries(REGISTRY[namespace] || {});
|
|
706
|
+
if (toolSet === 'all')
|
|
707
|
+
return all.map(([a]) => a);
|
|
708
|
+
// reporting/readOnly: include only underlying tools that start with 'get'
|
|
709
|
+
return all.filter(([, tool]) => tool.startsWith('get')).map(([a]) => a);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Compute MCP tool annotations from the filtered action set.
|
|
713
|
+
* Annotations reflect the worst-case capability of the exposed actions:
|
|
714
|
+
* - readOnlyHint: true only when ALL actions are reads
|
|
715
|
+
* - destructiveHint: true when ANY action is a delete
|
|
716
|
+
* - idempotentHint: true only when ALL actions are reads (GETs are idempotent)
|
|
717
|
+
* - openWorldHint: false — all tools affect bounded Convert API resources
|
|
718
|
+
*/
|
|
719
|
+
function annotationsForNamespace(ns, actions) {
|
|
720
|
+
const hasWriteAction = actions.some((a) => {
|
|
721
|
+
const toolName = REGISTRY[ns]?.[a];
|
|
722
|
+
return toolName && !toolName.startsWith('get');
|
|
723
|
+
});
|
|
724
|
+
const hasDeleteAction = actions.includes('delete');
|
|
725
|
+
return {
|
|
726
|
+
title: NAMESPACE_TITLES[ns] ?? ns,
|
|
727
|
+
readOnlyHint: !hasWriteAction,
|
|
728
|
+
destructiveHint: hasWriteAction && hasDeleteAction,
|
|
729
|
+
idempotentHint: !hasWriteAction,
|
|
730
|
+
openWorldHint: false,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
export function getNamespacedTools(toolSet) {
|
|
734
|
+
return Object.keys(REGISTRY).map((ns) => {
|
|
735
|
+
const actions = actionsForToolset(ns, toolSet);
|
|
736
|
+
// Build rich namespace description from curated map or fallback
|
|
737
|
+
const nsDesc = NAMESPACE_DESCRIPTIONS[ns] || `Manage ${ns} in the user's Convert.com account via the live API.`;
|
|
738
|
+
const description = `${nsDesc} Available actions: ${actions.join(', ')}.`;
|
|
739
|
+
// Build structured metadata for each action, merging ACTION_NOTES
|
|
740
|
+
const actionMetadata = actions.map((action) => {
|
|
741
|
+
const toolName = REGISTRY[ns][action];
|
|
742
|
+
const def = toolDefinitionMap.get(toolName);
|
|
743
|
+
if (!def)
|
|
744
|
+
return { action, description: 'No metadata available' };
|
|
745
|
+
const pathParams = requiredPathParamsForDef(def);
|
|
746
|
+
const queryParams = (def.executionParameters || [])
|
|
747
|
+
.filter((p) => p.in === 'query')
|
|
748
|
+
.map((p) => p.name);
|
|
749
|
+
const hasBody = !!def.requestBodyContentType;
|
|
750
|
+
const baseDesc = def.description || 'No description';
|
|
751
|
+
const note = ACTION_NOTES[`${ns}.${action}`];
|
|
752
|
+
const bodyHints = hasBody ? extractBodyHints(def.inputSchema) : null;
|
|
753
|
+
const parts = [baseDesc];
|
|
754
|
+
if (note)
|
|
755
|
+
parts.push(`Agent Notes:\n${note}`);
|
|
756
|
+
if (bodyHints)
|
|
757
|
+
parts.push(`Body schema:\n${bodyHints}`);
|
|
758
|
+
const fullDesc = parts.join('\n\n');
|
|
759
|
+
return {
|
|
760
|
+
action,
|
|
761
|
+
description: fullDesc,
|
|
762
|
+
method: def.method.toUpperCase(),
|
|
763
|
+
path_parameters: pathParams,
|
|
764
|
+
query_parameters: queryParams,
|
|
765
|
+
accepts_body: hasBody,
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
// Hint in action description when notes are available
|
|
769
|
+
const actionsWithNotes = actions.filter(a => ACTION_NOTES[`${ns}.${a}`]);
|
|
770
|
+
const actionDesc = actionsWithNotes.length
|
|
771
|
+
? 'Action to perform. See _meta.available_actions for detailed per-action guidance.'
|
|
772
|
+
: 'Action to perform';
|
|
773
|
+
return {
|
|
774
|
+
name: ns,
|
|
775
|
+
title: NAMESPACE_TITLES[ns] ?? ns,
|
|
776
|
+
description,
|
|
777
|
+
inputSchema: {
|
|
778
|
+
$schema: JSON_SCHEMA_DRAFT_2020_12,
|
|
779
|
+
type: 'object',
|
|
780
|
+
properties: {
|
|
781
|
+
action: { type: 'string', enum: actions, description: actionDesc },
|
|
782
|
+
path: { type: 'object', additionalProperties: true, description: 'Canonical wrapper for path params such as account_id, project_id, experience_id. For compatibility, known path params may also be supplied at the top level.' },
|
|
783
|
+
query: { type: 'object', additionalProperties: true, description: 'Canonical wrapper for query and optional header-style params (for example report_token). Known query/header params may also be supplied at the top level. Array parameters (like include, expand) must always be arrays, even for single values. IMPORTANT: When using "include" to fetch related resources, also use supported "expand" values to get complete object data instead of just numeric IDs. Some fields are includable but not expandable; namespace tools auto-fill valid expand values when possible. Example: {"include": ["goals"], "expand": ["goals"]}.' },
|
|
784
|
+
body: { type: 'object', additionalProperties: true, description: 'Canonical request body wrapper for create/update/clone actions. The namespace tool also accepts a top-level requestBody alias for compatibility.' },
|
|
785
|
+
confirmation: { type: 'object', additionalProperties: true, description: 'Optional explicit user confirmation metadata for sensitive actions. Example: {"user_confirmed": true, "reason": "User approved activation"}' },
|
|
786
|
+
},
|
|
787
|
+
required: ['action'],
|
|
788
|
+
_meta: {
|
|
789
|
+
namespace: ns,
|
|
790
|
+
available_actions: actionMetadata,
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
outputSchema: {
|
|
794
|
+
$schema: JSON_SCHEMA_DRAFT_2020_12,
|
|
795
|
+
type: 'object',
|
|
796
|
+
description: 'Standardized namespace tool result envelope.',
|
|
797
|
+
properties: {
|
|
798
|
+
ok: { type: 'boolean', description: 'True on success, false on tool-level error.' },
|
|
799
|
+
namespace: { type: 'string', description: 'Namespace that handled the request.' },
|
|
800
|
+
action: { type: 'string', description: 'Namespace action requested by the model.' },
|
|
801
|
+
delegate: { type: 'string', description: 'Underlying generated tool ID executed for this action.' },
|
|
802
|
+
status: { type: 'number', description: 'HTTP status code from the Convert API (when available).' },
|
|
803
|
+
data: { description: 'Raw API response payload for successful requests.' },
|
|
804
|
+
error: { description: 'Error code/object for failed requests.' },
|
|
805
|
+
issues: { type: 'array', items: { type: 'object' }, description: 'Validation issues, if present.' },
|
|
806
|
+
},
|
|
807
|
+
additionalProperties: true,
|
|
808
|
+
},
|
|
809
|
+
annotations: annotationsForNamespace(ns, actions),
|
|
810
|
+
};
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
function withNamespaceEnvelope(result, namespace, action, delegate) {
|
|
814
|
+
const base = (result.structuredContent && typeof result.structuredContent === 'object')
|
|
815
|
+
? result.structuredContent
|
|
816
|
+
: {};
|
|
817
|
+
const envelope = {
|
|
818
|
+
ok: !result.isError,
|
|
819
|
+
namespace,
|
|
820
|
+
...base,
|
|
821
|
+
};
|
|
822
|
+
if (action)
|
|
823
|
+
envelope.action = action;
|
|
824
|
+
if (delegate)
|
|
825
|
+
envelope.delegate = delegate;
|
|
826
|
+
if (result.isError && typeof envelope.error === 'undefined') {
|
|
827
|
+
const fallbackText = result.content?.[0]?.type === 'text' ? result.content[0].text : 'Unknown error';
|
|
828
|
+
envelope.error = fallbackText;
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
...result,
|
|
832
|
+
structuredContent: envelope,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
export async function executeNamespacedTool(namespace, args) {
|
|
836
|
+
if (!REGISTRY[namespace]) {
|
|
837
|
+
const error = { error: 'UnknownNamespace', namespace, available_namespaces: Object.keys(REGISTRY) };
|
|
838
|
+
return withNamespaceEnvelope({
|
|
839
|
+
content: [{ type: 'text', text: `Unknown namespace: ${namespace}` }],
|
|
840
|
+
structuredContent: error,
|
|
841
|
+
isError: true,
|
|
842
|
+
}, namespace);
|
|
843
|
+
}
|
|
844
|
+
const parsedArgs = asObject(coerceJsonLike(args));
|
|
845
|
+
if (!parsedArgs) {
|
|
846
|
+
const error = { error: 'InvalidArguments', message: 'Expected object with fields: action, path, query, body/requestBody, confirmation' };
|
|
847
|
+
return withNamespaceEnvelope({
|
|
848
|
+
content: [{ type: 'text', text: `Invalid arguments. Expected object with fields: action, path, query, body/requestBody, confirmation.` }],
|
|
849
|
+
structuredContent: error,
|
|
850
|
+
isError: true,
|
|
851
|
+
}, namespace);
|
|
852
|
+
}
|
|
853
|
+
const action = parsedArgs.action;
|
|
854
|
+
if (!action || typeof action !== 'string') {
|
|
855
|
+
const error = { error: 'MissingAction', message: 'The action field is required and must be a string' };
|
|
856
|
+
return withNamespaceEnvelope({
|
|
857
|
+
content: [{ type: 'text', text: `Missing or invalid 'action'.` }],
|
|
858
|
+
structuredContent: error,
|
|
859
|
+
isError: true,
|
|
860
|
+
}, namespace);
|
|
861
|
+
}
|
|
862
|
+
const delegate = REGISTRY[namespace][action];
|
|
863
|
+
if (!delegate) {
|
|
864
|
+
const available = Object.keys(REGISTRY[namespace]);
|
|
865
|
+
const error = { error: 'UnknownAction', namespace, action, available_actions: available };
|
|
866
|
+
return withNamespaceEnvelope({
|
|
867
|
+
content: [{ type: 'text', text: `Unknown action '${action}' for namespace '${namespace}'. Available: ${available.join(', ')}` }],
|
|
868
|
+
structuredContent: error,
|
|
869
|
+
isError: true,
|
|
870
|
+
}, namespace, action);
|
|
871
|
+
}
|
|
872
|
+
const def = toolDefinitionMap.get(delegate);
|
|
873
|
+
if (!def) {
|
|
874
|
+
const error = { error: 'InternalError', message: 'Delegate tool not found', delegate };
|
|
875
|
+
return withNamespaceEnvelope({
|
|
876
|
+
content: [{ type: 'text', text: `Internal error: delegate tool not found: ${delegate}` }],
|
|
877
|
+
structuredContent: error,
|
|
878
|
+
isError: true,
|
|
879
|
+
}, namespace, action, delegate);
|
|
880
|
+
}
|
|
881
|
+
const { path, query, body, confirmation } = normalizeNamespaceArgs(parsedArgs, def);
|
|
882
|
+
// Validate required path params early for better error messages
|
|
883
|
+
const requiredPathParams = requiredPathParamsForDef(def);
|
|
884
|
+
const missing = requiredPathParams.filter((k) => !(k in path));
|
|
885
|
+
if (missing.length) {
|
|
886
|
+
const note = ACTION_NOTES[`${namespace}.${action}`];
|
|
887
|
+
const hint = note ? `\n\nHint for ${namespace}.${action}:\n${note}` : '';
|
|
888
|
+
const error = { error: 'MissingPathParameters', namespace, action, missing_parameters: missing };
|
|
889
|
+
return withNamespaceEnvelope({
|
|
890
|
+
content: [{ type: 'text', text: `Missing required path parameters: ${missing.map(k => `path.${k}`).join(', ')}${hint}` }],
|
|
891
|
+
structuredContent: error,
|
|
892
|
+
isError: true,
|
|
893
|
+
}, namespace, action, delegate);
|
|
894
|
+
}
|
|
895
|
+
const normalizedBody = normalizeBodyForAction(namespace, action, body);
|
|
896
|
+
const guardrail = evaluateGuardrails(namespace, action, normalizedBody, confirmation, path);
|
|
897
|
+
if (guardrail) {
|
|
898
|
+
return withNamespaceEnvelope({
|
|
899
|
+
content: [{ type: 'text', text: guardrail.message }],
|
|
900
|
+
structuredContent: {
|
|
901
|
+
error: guardrail.code,
|
|
902
|
+
namespace,
|
|
903
|
+
action,
|
|
904
|
+
message: guardrail.message,
|
|
905
|
+
...(guardrail.suggested_call ? { suggested_call: guardrail.suggested_call } : {}),
|
|
906
|
+
},
|
|
907
|
+
isError: true,
|
|
908
|
+
}, namespace, action, delegate);
|
|
909
|
+
}
|
|
910
|
+
const flattenedArgs = { ...path, ...query };
|
|
911
|
+
if (def.requestBodyContentType && typeof normalizedBody !== 'undefined') {
|
|
912
|
+
flattenedArgs.requestBody = normalizedBody;
|
|
913
|
+
}
|
|
914
|
+
const result = await executeToolByName(delegate, flattenedArgs);
|
|
915
|
+
// Append action-specific guidance to error messages so agents can self-correct
|
|
916
|
+
if (result.isError) {
|
|
917
|
+
const note = ACTION_NOTES[`${namespace}.${action}`];
|
|
918
|
+
if (note && result.content?.[0]?.type === 'text') {
|
|
919
|
+
result.content[0].text += `\n\nHint for ${namespace}.${action}:\n${note}`;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return withNamespaceEnvelope(result, namespace, action, delegate);
|
|
923
|
+
}
|
|
924
|
+
export function isNamespace(name) {
|
|
925
|
+
return Object.prototype.hasOwnProperty.call(REGISTRY, name);
|
|
926
|
+
}
|
|
927
|
+
//# sourceMappingURL=namespaced.js.map
|