@velaro/mcp-server 0.6.68 → 0.6.70

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 (3) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/server.js +404 -4
package/README.md CHANGED
@@ -167,6 +167,7 @@ This works with any MCP client that supports HTTP/SSE transport — including Cl
167
167
  |------|-------------|
168
168
  | `report_roi_summary` | Full ROI report — bot deflections, hours saved, cost savings, AI skill executions, resolution rate, response time, trends vs prior period, monthly breakdown, top skills by call count, channel breakdown |
169
169
  | `report_agent_utilization` | Per-agent stats — conversation count, avg handle time, active/away breakdown for any date range |
170
+ | `report_visitor_traffic_overview` | Visitor Traffic report (velaro-messaging) — daily visit trend, top countries and chat-channel breakdown for any date range |
170
171
  | `report_campaign_performance` | Email campaign KPIs — open rate, click rate, delivery rate, complaint rate per campaign + aggregate totals |
171
172
  | `report_campaign_deliverability` | Deliverability health check — bounce rate, complaint rate, unsubscribe rate, open rate with traffic-light status and fix recommendations |
172
173
  | `report_campaign_monthly` | Month-by-month email volume for the last 12 months |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "0.6.68",
3
+ "version": "0.6.70",
4
4
  "description": "Velaro MCP server — connect Claude and other AI agents directly to your Velaro account: KB, workflows, bots, conversations, contacts, routing, and more.",
5
5
  "type": "module",
6
6
  "bin": {
package/server.js CHANGED
@@ -119,12 +119,18 @@ function parseDuration(s) {
119
119
  return n * 24 * 3600 * 1000;
120
120
  }
121
121
 
122
- async function messagingApi(path) {
122
+ async function messagingApi(path, opts = {}) {
123
123
  if (!MESSAGING_API) throw new Error(
124
124
  'VELARO_MESSAGING_API is not configured. Add "VELARO_MESSAGING_API": "https://velaro-messaging-api-staging.azurewebsites.net" to your MCP env config.'
125
125
  );
126
126
  const res = await fetch(`${MESSAGING_API}${path}`, {
127
- headers: { Authorization: authHeader(), 'X-Internal-SiteId': '1032' },
127
+ method: opts.method ?? 'GET',
128
+ headers: {
129
+ Authorization: authHeader(),
130
+ 'X-Internal-SiteId': '1032',
131
+ ...(opts.body ? { 'Content-Type': 'application/json' } : {}),
132
+ },
133
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
128
134
  signal: AbortSignal.timeout(20000),
129
135
  });
130
136
  if (!res.ok) {
@@ -525,9 +531,59 @@ const TOOLS = [
525
531
  required: ['id', 'title', 'slug', 'topicId', 'content'],
526
532
  },
527
533
  },
534
+ {
535
+ name: 'kb_list_deleted_articles',
536
+ description: 'List recently deleted KB articles (restorable for 30 days).',
537
+ inputSchema: { type: 'object', properties: {} },
538
+ },
539
+ {
540
+ name: 'kb_restore_article',
541
+ description: 'Restore a deleted KB article (and re-index it).',
542
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Article ID' } }, required: ['id'] },
543
+ },
544
+ {
545
+ name: 'kb_list_deleted_topics',
546
+ description: 'List recently deleted KB topics (restorable for 30 days).',
547
+ inputSchema: { type: 'object', properties: {} },
548
+ },
549
+ {
550
+ name: 'kb_restore_topic',
551
+ description: 'Restore a deleted KB topic.',
552
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Topic ID' } }, required: ['id'] },
553
+ },
554
+ {
555
+ name: 'contactlist_list_deleted',
556
+ description: 'List recently deleted contact lists (restorable for 30 days).',
557
+ inputSchema: { type: 'object', properties: {} },
558
+ },
559
+ {
560
+ name: 'contactlist_restore',
561
+ description: 'Restore a deleted contact list with its members.',
562
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'List ID' } }, required: ['id'] },
563
+ },
564
+ {
565
+ name: 'contactsegment_list_deleted',
566
+ description: 'List recently deleted contact segments (restorable for 30 days).',
567
+ inputSchema: { type: 'object', properties: {} },
568
+ },
569
+ {
570
+ name: 'contactsegment_restore',
571
+ description: 'Restore a deleted contact segment.',
572
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Segment ID' } }, required: ['id'] },
573
+ },
574
+ {
575
+ name: 'contactlist_member_list_deleted',
576
+ description: 'List members recently removed from a contact list (restorable for 30 days).',
577
+ inputSchema: { type: 'object', properties: { listId: { type: 'number' } }, required: ['listId'] },
578
+ },
579
+ {
580
+ name: 'contactlist_member_restore',
581
+ description: 'Restore a member removed from a contact list.',
582
+ inputSchema: { type: 'object', properties: { listId: { type: 'number' }, memberId: { type: 'number' } }, required: ['listId', 'memberId'] },
583
+ },
528
584
  {
529
585
  name: 'kb_delete_article',
530
- description: 'Delete a KB article. Irreversible — confirm with the user before calling.',
586
+ description: 'Delete a KB article (soft delete: restorable for 30 days via kb_restore_article, then purged).',
531
587
  inputSchema: {
532
588
  type: 'object',
533
589
  properties: { id: { type: 'number', description: 'Article ID to delete' } },
@@ -2017,6 +2073,17 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2017
2073
  },
2018
2074
  },
2019
2075
  },
2076
+ {
2077
+ name: 'report_visitor_traffic_overview',
2078
+ description: 'Visitor Traffic report (velaro-messaging) — daily visit trend, top countries and chat-channel breakdown over a date range. Site is resolved server-side from the service key/JWT, never a client-supplied value. Lives in velaro-messaging — requires VELARO_MESSAGING_API.',
2079
+ inputSchema: {
2080
+ type: 'object',
2081
+ properties: {
2082
+ startDate: { type: 'string', description: 'ISO 8601 start date, e.g. 2026-01-01 (default: 30 days ago)' },
2083
+ endDate: { type: 'string', description: 'ISO 8601 end date, e.g. 2026-04-19 (default: now)' },
2084
+ },
2085
+ },
2086
+ },
2020
2087
  {
2021
2088
  name: 'report_schedules_get',
2022
2089
  description: 'List the recurring report email schedules configured for this site (Reports/Schedules) — report type, frequency, recipients, enabled state.',
@@ -2197,6 +2264,37 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2197
2264
  },
2198
2265
  },
2199
2266
 
2267
+ // ── Klaviyo migration ─────────────────────────────────────────────────────
2268
+ {
2269
+ name: 'klaviyo_migration_run',
2270
+ description: 'Import the customer\'s Klaviyo lists, segments and flows into Velaro (uses the key stored by the Klaviyo integration). SAFE BY DEFAULT: dryRun defaults to true and writes nothing; pass dryRun=false only after reviewing the preview. Idempotent. Only SUBSCRIBED profiles are imported; flows arrive inactive.',
2271
+ inputSchema: {
2272
+ type: 'object',
2273
+ properties: {
2274
+ dryRun: { type: 'boolean', description: 'Default true. false = apply the import.' },
2275
+ listIds: { type: 'array', items: { type: 'string' }, description: 'Klaviyo list ids (omit for all)' },
2276
+ segmentIds: { type: 'array', items: { type: 'string' }, description: 'Klaviyo segment ids (omit for all)' },
2277
+ flowIds: { type: 'array', items: { type: 'string' }, description: 'Klaviyo flow ids (omit for all)' },
2278
+ includeFlows: { type: 'boolean', description: 'Default true' },
2279
+ maxProfilesPerGroup: { type: 'number', description: 'Cap per list/segment (default 5000)' },
2280
+ },
2281
+ required: [],
2282
+ },
2283
+ },
2284
+ {
2285
+ name: 'klaviyo_savings_calculator',
2286
+ description: 'Compare a Klaviyo monthly bill to a Velaro plan\'s catalog price. Velaro price comes only from the pricing catalog. Both profiles and klaviyoMonthlyBill are REQUIRED.',
2287
+ inputSchema: {
2288
+ type: 'object',
2289
+ properties: {
2290
+ profiles: { type: 'number', description: 'Klaviyo profile count' },
2291
+ klaviyoMonthlyBill: { type: 'number', description: 'Current Klaviyo monthly bill in USD' },
2292
+ planKey: { type: 'string', description: 'Velaro plan key, default plan.professional' },
2293
+ },
2294
+ required: ['profiles', 'klaviyoMonthlyBill'],
2295
+ },
2296
+ },
2297
+
2200
2298
  // ── Conversion tracking tools ─────────────────────────────────────────────
2201
2299
  {
2202
2300
  name: 'conversion_list_goals',
@@ -2329,6 +2427,34 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2329
2427
  },
2330
2428
  },
2331
2429
 
2430
+ {
2431
+ name: 'ai_model_set_site_thinking',
2432
+ description: 'Set the extended-thinking level for a site (off | low | medium | high), optionally scoped to one role. Staff only. Thinking bills as output tokens, is off by default, and only takes effect when the site holds the EnableAiThinking entitlement. Opus 5.5 always thinks at model-default effort regardless.',
2433
+ inputSchema: {
2434
+ type: 'object',
2435
+ properties: {
2436
+ siteId: { type: 'number', description: 'Site ID' },
2437
+ effort: { type: 'string', enum: ['off', 'low', 'medium', 'high'], description: 'Thinking level' },
2438
+ role: { type: 'string', description: 'Optional role to scope the setting to (e.g. chatbot). Omit for the site-wide value.' },
2439
+ },
2440
+ required: ['siteId', 'effort'],
2441
+ },
2442
+ },
2443
+ {
2444
+ name: 'ai_model_set_site_pin',
2445
+ description: 'Set a per-site role model override and choose whether to pin its exact version (pinVersion=true keeps a still-callable legacy model like claude-opus-5 instead of auto-upgrading to the latest). Staff only. Non-pinnable ids ignore the pin.',
2446
+ inputSchema: {
2447
+ type: 'object',
2448
+ properties: {
2449
+ siteId: { type: 'number', description: 'Site ID' },
2450
+ role: { type: 'string', description: 'Role (e.g. chatbot, ivr, skill_synthesis)' },
2451
+ model: { type: 'string', description: 'Model id/alias to set for this role' },
2452
+ pinVersion: { type: 'boolean', description: 'true = keep this exact model, do not auto-upgrade' },
2453
+ },
2454
+ required: ['siteId', 'role', 'model', 'pinVersion'],
2455
+ },
2456
+ },
2457
+
2332
2458
  // ── Page Widgets ──────────────────────────────────────────────────────────
2333
2459
  {
2334
2460
  name: 'page_widget_list',
@@ -2638,6 +2764,30 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2638
2764
  required: ['siteId'],
2639
2765
  },
2640
2766
  },
2767
+ {
2768
+ name: 'support_create_agent',
2769
+ description: 'Provision a new licensed agent/admin (Entra/V20 WorkspaceUser) on an EXISTING site. ' +
2770
+ 'Velaro staff only. Closes a real gap: site-provision only creates brand-new sites (rejects an ' +
2771
+ 'ExplicitSiteId that already exists), and the ordinary Users/List create endpoint requires an ' +
2772
+ 'existing site admin to call it -- impossible on a zero-agent site (a fresh review/demo site, or ' +
2773
+ 'any site whose only admin left). Enforces the site\'s license/seat cap same as the ordinary ' +
2774
+ 'create-user flow. "Velaro Admin"/"Velaro Support" roles are always stripped server-side -- this ' +
2775
+ 'tool provisions customer/site agents, never internal staff privilege escalation. Entra/V20 sites ' +
2776
+ 'only (does not cover a site still on the legacy V10 identity model).',
2777
+ inputSchema: {
2778
+ type: 'object',
2779
+ properties: {
2780
+ siteId: { type: 'number', description: 'Target site ID (must already exist)' },
2781
+ email: { type: 'string', description: 'New agent email' },
2782
+ firstName: { type: 'string', description: 'First name' },
2783
+ lastName: { type: 'string', description: 'Last name' },
2784
+ roles: { type: 'array', items: { type: 'string' }, description: 'Roles to grant (default: ["Administrator"])' },
2785
+ teamIds: { type: 'array', items: { type: 'number' }, description: 'Team IDs on the target site (default: the site\'s first team)' },
2786
+ maxChats: { type: 'number', description: 'Concurrent chat capacity (default: site ceiling)' },
2787
+ },
2788
+ required: ['siteId', 'email'],
2789
+ },
2790
+ },
2641
2791
  {
2642
2792
  name: 'support_site_activity',
2643
2793
  description: 'Get recent activity log entries for a customer site — who did what and when (workflows, logins, settings changes, etc.). Velaro staff only.',
@@ -2756,6 +2906,59 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2756
2906
  },
2757
2907
  },
2758
2908
 
2909
+ // ── Velaro Avatar (first-party provider) — library, personalities, site/group inheritance ──
2910
+ {
2911
+ name: 'avatar_list_library',
2912
+ description: 'List Velaro Avatar library entries (realistic and character) with id slug, name, kind, hasImage, default personality. Lives in velaro-messaging — requires VELARO_MESSAGING_API.',
2913
+ inputSchema: { type: 'object', properties: { kind: { type: 'string', description: '"realistic" or "character". Omit for all.' } } },
2914
+ },
2915
+ {
2916
+ name: 'avatar_list_personalities',
2917
+ description: 'List Velaro Avatar personality presets (tone/style) that can be selected at site or group level.',
2918
+ inputSchema: { type: 'object', properties: {} },
2919
+ },
2920
+ {
2921
+ name: 'avatar_set_site_default',
2922
+ description: 'Set the calling site\'s default avatar provider, avatar and personality (applies to all groups without an override). Provider "velaro" is the default Velaro-owned avatar; "simli", "d-id", "heygen", "none" also valid.',
2923
+ inputSchema: {
2924
+ type: 'object',
2925
+ properties: {
2926
+ providerSlug: { type: 'string', description: 'velaro | simli | d-id | heygen | none' },
2927
+ avatarId: { type: 'string', description: 'Library slug when provider is velaro, else the provider avatar id.' },
2928
+ personalityPresetSlug: { type: 'string', description: 'Personality preset slug.' },
2929
+ },
2930
+ },
2931
+ },
2932
+ {
2933
+ name: 'avatar_get_group_effective',
2934
+ description: 'Show the effective avatar settings for a group (group override field, else site default) and whether an override exists.',
2935
+ inputSchema: { type: 'object', properties: { groupId: { type: 'number' } }, required: ['groupId'] },
2936
+ },
2937
+ {
2938
+ name: 'avatar_set_group_override',
2939
+ description: 'Override avatar/personality/provider for one group. Omitted fields inherit the site default.',
2940
+ inputSchema: {
2941
+ type: 'object',
2942
+ properties: {
2943
+ groupId: { type: 'number' },
2944
+ providerSlug: { type: 'string' },
2945
+ avatarId: { type: 'string' },
2946
+ personalityPresetSlug: { type: 'string' },
2947
+ },
2948
+ required: ['groupId'],
2949
+ },
2950
+ },
2951
+ {
2952
+ name: 'avatar_reset_group',
2953
+ description: 'Reset one group to the site default avatar settings (removes its override).',
2954
+ inputSchema: { type: 'object', properties: { groupId: { type: 'number' } }, required: ['groupId'] },
2955
+ },
2956
+ {
2957
+ name: 'avatar_reset_all_overrides',
2958
+ description: 'Reset ALL group overrides for the calling site back to the site default.',
2959
+ inputSchema: { type: 'object', properties: {} },
2960
+ },
2961
+
2759
2962
  // ── Calendly integration management (superadmin) ──────────────────────────
2760
2963
  {
2761
2964
  name: 'calendly_get_status',
@@ -4596,6 +4799,37 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
4596
4799
  required: ['ruleId'],
4597
4800
  },
4598
4801
  },
4802
+ {
4803
+ name: 'booking_list_appointments',
4804
+ description: 'List actual bookings (Phase 1c) made against the booking engine for this site, optionally filtered by date range or service.',
4805
+ inputSchema: {
4806
+ type: 'object',
4807
+ properties: {
4808
+ fromUtc: { type: 'string', description: 'Only bookings starting at or after this ISO 8601 UTC timestamp' },
4809
+ toUtc: { type: 'string', description: 'Only bookings starting at or before this ISO 8601 UTC timestamp' },
4810
+ bookableServiceId: { type: 'number', description: 'Only bookings for this bookable service id' },
4811
+ },
4812
+ required: [],
4813
+ },
4814
+ },
4815
+ {
4816
+ name: 'booking_get_appointment',
4817
+ description: 'Get a single booking (Phase 1c) by id.',
4818
+ inputSchema: {
4819
+ type: 'object',
4820
+ properties: { bookingId: { type: 'number', description: 'Booking ID. Required.' } },
4821
+ required: ['bookingId'],
4822
+ },
4823
+ },
4824
+ {
4825
+ name: 'booking_cancel_appointment',
4826
+ description: 'Cancel a booking (Phase 1c), staff-initiated. Releases the held slot capacity.',
4827
+ inputSchema: {
4828
+ type: 'object',
4829
+ properties: { bookingId: { type: 'number', description: 'Booking ID. Required.' } },
4830
+ required: ['bookingId'],
4831
+ },
4832
+ },
4599
4833
 
4600
4834
  // Focus Inbox -- draft pregeneration usage/cap report ("Always Learning" addon,
4601
4835
  // issue #582 Phase D). Entitlements-gated (EnableFocusDraftPregeneration), not a
@@ -4943,6 +5177,61 @@ async function handleTool(name, args) {
4943
5177
  return `Updated KB settings. ${JSON.stringify(args)}`;
4944
5178
  }
4945
5179
 
5180
+ case 'kb_list_deleted_articles': {
5181
+ const rows = await api('GET', `/api/kb/articles/deleted`);
5182
+ if (!Array.isArray(rows) || !rows.length) return 'Nothing in Recently deleted.';
5183
+ return rows.map(r => `#${r.id} ${r.name} (deleted ${r.deletedAt}, purges in ${r.daysUntilPurge} day(s))`).join('\n');
5184
+ }
5185
+
5186
+ case 'kb_restore_article': {
5187
+ await api('POST', `/api/kb/articles/${args.id}/restore`);
5188
+ return `Restored #${args.id}.`;
5189
+ }
5190
+
5191
+ case 'kb_list_deleted_topics': {
5192
+ const rows = await api('GET', `/api/kb/topics/deleted`);
5193
+ if (!Array.isArray(rows) || !rows.length) return 'Nothing in Recently deleted.';
5194
+ return rows.map(r => `#${r.id} ${r.name} (deleted ${r.deletedAt}, purges in ${r.daysUntilPurge} day(s))`).join('\n');
5195
+ }
5196
+
5197
+ case 'kb_restore_topic': {
5198
+ await api('POST', `/api/kb/topics/${args.id}/restore`);
5199
+ return `Restored #${args.id}.`;
5200
+ }
5201
+
5202
+ case 'contactlist_list_deleted': {
5203
+ const rows = await api('GET', `/ContactLists/deleted`);
5204
+ if (!Array.isArray(rows) || !rows.length) return 'Nothing in Recently deleted.';
5205
+ return rows.map(r => `#${r.id} ${r.name} (deleted ${r.deletedAt}, purges in ${r.daysUntilPurge} day(s))`).join('\n');
5206
+ }
5207
+
5208
+ case 'contactlist_restore': {
5209
+ await api('POST', `/ContactLists/${args.id}/restore`);
5210
+ return `Restored #${args.id}.`;
5211
+ }
5212
+
5213
+ case 'contactsegment_list_deleted': {
5214
+ const rows = await api('GET', `/ContactSegments/deleted`);
5215
+ if (!Array.isArray(rows) || !rows.length) return 'Nothing in Recently deleted.';
5216
+ return rows.map(r => `#${r.id} ${r.name} (deleted ${r.deletedAt}, purges in ${r.daysUntilPurge} day(s))`).join('\n');
5217
+ }
5218
+
5219
+ case 'contactsegment_restore': {
5220
+ await api('POST', `/ContactSegments/${args.id}/restore`);
5221
+ return `Restored #${args.id}.`;
5222
+ }
5223
+
5224
+ case 'contactlist_member_list_deleted': {
5225
+ const rows = await api('GET', `/ContactLists/${args.listId}/Members/deleted`);
5226
+ if (!Array.isArray(rows) || !rows.length) return 'Nothing in Recently deleted.';
5227
+ return rows.map(r => `#${r.id} ${r.name} (purges in ${r.daysUntilPurge} day(s))`).join('\n');
5228
+ }
5229
+
5230
+ case 'contactlist_member_restore': {
5231
+ await api('POST', `/ContactLists/${args.listId}/Members/${args.memberId}/restore`);
5232
+ return `Restored member #${args.memberId}.`;
5233
+ }
5234
+
4946
5235
  case 'kb_delete_article': {
4947
5236
  await api('DELETE', `/api/kb/articles?id=${args.id}`);
4948
5237
  return `Article ${args.id} deleted.`;
@@ -5004,6 +5293,18 @@ async function handleTool(name, args) {
5004
5293
  return JSON.stringify(h, null, 2);
5005
5294
  }
5006
5295
 
5296
+ case 'support_create_agent': {
5297
+ const r = await messagingPost(`/SupportTools/sites/${args.siteId}/agents`, {
5298
+ email: args.email,
5299
+ firstName: args.firstName,
5300
+ lastName: args.lastName,
5301
+ roles: args.roles,
5302
+ teamIds: args.teamIds,
5303
+ maxChats: args.maxChats ?? 0,
5304
+ });
5305
+ return JSON.stringify(r, null, 2);
5306
+ }
5307
+
5007
5308
  case 'apple_test_control': {
5008
5309
  const r = await messagingPost(`/SupportTools/sites/${args.siteId}/apple/test-control`, {
5009
5310
  control: args.control,
@@ -6481,6 +6782,31 @@ async function handleTool(name, args) {
6481
6782
  return lines.join('\n');
6482
6783
  }
6483
6784
 
6785
+ case 'report_visitor_traffic_overview': {
6786
+ const end = args.endDate ? new Date(args.endDate) : new Date();
6787
+ const start = args.startDate ? new Date(args.startDate) : new Date(end.getTime() - 30 * 24 * 3600 * 1000);
6788
+ const r = await messagingPost('/reports/visitor-traffic-overview', {
6789
+ start: start.toISOString(),
6790
+ end: end.toISOString(),
6791
+ });
6792
+ const k = r?.kpis ?? {};
6793
+ const lines = [
6794
+ `=== Visitor Traffic (${r?.start ?? start.toISOString().slice(0, 10)} – ${r?.end ?? end.toISOString().slice(0, 10)}) ===`,
6795
+ `Total Visits: ${k.totalVisits ?? 0} | Avg Daily: ${k.avgDailyVisits ?? 0} | Countries Reached: ${k.countriesReached ?? 0} | Chats Started: ${k.chatsStarted ?? 0}`,
6796
+ '',
6797
+ '--- Top Countries ---',
6798
+ ...(Array.isArray(r?.countryBreakdown) && r.countryBreakdown.length
6799
+ ? r.countryBreakdown.map((c) => ` ${c.country}: ${c.count}`)
6800
+ : [' (no data)']),
6801
+ '',
6802
+ '--- By Channel ---',
6803
+ ...(Array.isArray(r?.channelBreakdown) && r.channelBreakdown.length
6804
+ ? r.channelBreakdown.map((c) => ` ${c.name}: ${c.count} (${c.pct}%)`)
6805
+ : [' (no data)']),
6806
+ ];
6807
+ return lines.join('\n');
6808
+ }
6809
+
6484
6810
  case 'report_schedules_get': {
6485
6811
  const rows = await api('GET', '/Reports/Schedules');
6486
6812
  if (!Array.isArray(rows) || !rows.length) return 'No scheduled reports configured for this site.';
@@ -6920,6 +7246,25 @@ async function handleTool(name, args) {
6920
7246
  return JSON.stringify(r, null, 2);
6921
7247
  }
6922
7248
 
7249
+ // ── Klaviyo migration ──────────────────────────────────────────────────
7250
+ case 'klaviyo_migration_run': {
7251
+ // Anything other than an explicit boolean false stays a dry run.
7252
+ const dryRun = args.dryRun !== false;
7253
+ const r = await api('POST', `/KlaviyoMigration/${dryRun ? 'Preview' : 'Apply'}`, {
7254
+ listIds: args.listIds, segmentIds: args.segmentIds, flowIds: args.flowIds,
7255
+ includeFlows: args.includeFlows, maxProfilesPerGroup: args.maxProfilesPerGroup,
7256
+ });
7257
+ return JSON.stringify(r, null, 2);
7258
+ }
7259
+
7260
+ case 'klaviyo_savings_calculator': {
7261
+ if (args.profiles == null || args.klaviyoMonthlyBill == null) return 'Error: profiles and klaviyoMonthlyBill are required';
7262
+ const p = new URLSearchParams({ profiles: String(args.profiles), klaviyoMonthlyBill: String(args.klaviyoMonthlyBill) });
7263
+ if (args.planKey) p.set('planKey', args.planKey);
7264
+ const r = await api('GET', `/KlaviyoMigration/SavingsCalculator?${p}`);
7265
+ return JSON.stringify(r, null, 2);
7266
+ }
7267
+
6923
7268
  // ── Unified Outbound Activity ──────────────────────────────────────────
6924
7269
  case 'outbound_activity_list':
6925
7270
  case 'outbound_activity_for_contact': {
@@ -7126,6 +7471,22 @@ async function handleTool(name, args) {
7126
7471
  return lines.join('\n');
7127
7472
  }
7128
7473
 
7474
+ case 'ai_model_set_site_thinking': {
7475
+ const data = await messagingApi(`/superadmin/ai-models/site/${Number(args.siteId)}/thinking`, {
7476
+ method: 'POST',
7477
+ body: { effort: args.effort, ...(args.role ? { role: args.role } : {}) },
7478
+ });
7479
+ return `Site ${data.siteId}: ${data.key} = ${data.effort}. (Only active if the site holds the EnableAiThinking entitlement.)`;
7480
+ }
7481
+
7482
+ case 'ai_model_set_site_pin': {
7483
+ const data = await messagingApi(`/superadmin/ai-models/site/${Number(args.siteId)}`, {
7484
+ method: 'POST',
7485
+ body: { role: args.role, model: args.model, pinVersion: args.pinVersion === true, updatedBy: 'velaro-mcp' },
7486
+ });
7487
+ return `Site ${data.siteId} role ${data.role}: model ${data.model}, pinVersion=${data.pinVersion}`;
7488
+ }
7489
+
7129
7490
  // ── Page Widgets ──────────────────────────────────────────────────────
7130
7491
  case 'page_widget_list': {
7131
7492
  const data = await api('GET', '/PageWidgets');
@@ -7488,6 +7849,23 @@ async function handleTool(name, args) {
7488
7849
  return `✓ AI Video Avatar updated for site ${args.siteId}. Provider: ${res.providerSlug}, enabled: ${res.enabled}`;
7489
7850
  }
7490
7851
 
7852
+ case 'avatar_list_library':
7853
+ return JSON.stringify(await messagingRequest('GET', `/avatar/library${args.kind ? `?kind=${encodeURIComponent(args.kind)}` : ''}`), null, 2);
7854
+ case 'avatar_list_personalities':
7855
+ return JSON.stringify(await messagingRequest('GET', '/avatar/personalities'), null, 2);
7856
+ case 'avatar_set_site_default':
7857
+ return JSON.stringify(await messagingRequest('POST', '/avatar/site-default', {
7858
+ providerSlug: args.providerSlug, avatarId: args.avatarId, personalityPresetSlug: args.personalityPresetSlug }), null, 2);
7859
+ case 'avatar_get_group_effective':
7860
+ return JSON.stringify(await messagingRequest('GET', `/avatar/groups/${Number(args.groupId)}/effective`), null, 2);
7861
+ case 'avatar_set_group_override':
7862
+ return JSON.stringify(await messagingRequest('PUT', `/avatar/groups/${Number(args.groupId)}/override`, {
7863
+ providerSlug: args.providerSlug, avatarId: args.avatarId, personalityPresetSlug: args.personalityPresetSlug }), null, 2);
7864
+ case 'avatar_reset_group':
7865
+ return JSON.stringify(await messagingRequest('DELETE', `/avatar/groups/${Number(args.groupId)}/override`), null, 2);
7866
+ case 'avatar_reset_all_overrides':
7867
+ return JSON.stringify(await messagingRequest('POST', '/avatar/group-overrides/reset-all', {}), null, 2);
7868
+
7491
7869
  case 'calendly_get_status': {
7492
7870
  // `env`, when given, targets one specific admin API environment instead of whatever
7493
7871
  // VELARO_ADMIN_API this server process happens to be configured with — see ADMIN_API_HOSTS
@@ -9183,6 +9561,28 @@ async function handleTool(name, args) {
9183
9561
  return JSON.stringify(result, null, 2);
9184
9562
  }
9185
9563
 
9564
+ case 'booking_list_appointments': {
9565
+ const params = [];
9566
+ if (args.fromUtc != null) params.push(`fromUtc=${encodeURIComponent(args.fromUtc)}`);
9567
+ if (args.toUtc != null) params.push(`toUtc=${encodeURIComponent(args.toUtc)}`);
9568
+ if (args.bookableServiceId != null) params.push(`bookableServiceId=${encodeURIComponent(args.bookableServiceId)}`);
9569
+ const qs = params.length ? `?${params.join('&')}` : '';
9570
+ const result = await messagingGet(`/Bookings${qs}`);
9571
+ return JSON.stringify(result, null, 2);
9572
+ }
9573
+
9574
+ case 'booking_get_appointment': {
9575
+ if (args.bookingId == null) return 'bookingId is required.';
9576
+ const result = await messagingGet(`/Bookings/${encodeURIComponent(args.bookingId)}`);
9577
+ return JSON.stringify(result, null, 2);
9578
+ }
9579
+
9580
+ case 'booking_cancel_appointment': {
9581
+ if (args.bookingId == null) return 'bookingId is required.';
9582
+ const result = await messagingPost(`/Bookings/${encodeURIComponent(args.bookingId)}/cancel`, {});
9583
+ return JSON.stringify(result, null, 2);
9584
+ }
9585
+
9186
9586
  default:
9187
9587
  throw new Error(`Unknown tool: ${name}`);
9188
9588
  }
@@ -9283,4 +9683,4 @@ if (mode === 'http') {
9283
9683
  } else {
9284
9684
  const transport = new StdioServerTransport();
9285
9685
  await server.connect(transport);
9286
- }
9686
+ }