@velaro/mcp-server 2.0.3 → 2.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
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": {
@@ -0,0 +1,87 @@
1
+ // MCP parity for `velaro agent crm-identity` (cli/lib/commands/agent.js). Backend: velaro-messaging
2
+ // api/v1/AgentCrmIdentities (administrator key). Site from the credential, never an argument.
3
+ // A CRM login (NetSuite / Dynamics 365 / HubSpot user) linked to a Velaro agent makes the CRM call pop show that
4
+ // agent's calls and makes calls logged to the CRM owned by that CRM user.
5
+
6
+ const PROVIDERS = ['netsuite', 'dynamics', 'hubspot', 'salesforce', 'zoho', 'other'];
7
+
8
+ const agentProps = {
9
+ workspaceUserId: { type: 'number', description: 'Velaro agent id (WorkspaceUser id). Use this or agentEmail.' },
10
+ agentEmail: { type: 'string', description: 'Velaro agent email. Use this or workspaceUserId.' },
11
+ };
12
+
13
+ export const tools = [
14
+ {
15
+ name: 'agent_crm_identity_list',
16
+ description: 'List the CRM logins linked to your agents (all agents, or one agent). Each row: CRM, login email, CRM user id, agent, and whether it was linked manually or automatically.',
17
+ inputSchema: { type: 'object', properties: { workspaceUserId: agentProps.workspaceUserId } },
18
+ },
19
+ {
20
+ name: 'agent_crm_identity_add',
21
+ description: 'Link a CRM login to a Velaro agent so the CRM call pop shows that agent\'s calls and calls logged to the CRM are owned by that CRM user. Give the login email, the CRM user id (NetSuite employee id, Dynamics systemuserid, HubSpot owner id), or both.',
22
+ inputSchema: {
23
+ type: 'object',
24
+ properties: {
25
+ ...agentProps,
26
+ provider: { type: 'string', enum: PROVIDERS, description: 'Which CRM the login belongs to.' },
27
+ loginEmail: { type: 'string', description: 'The CRM login email.' },
28
+ crmUserId: { type: 'string', description: 'The CRM user id.' },
29
+ },
30
+ required: ['provider'],
31
+ },
32
+ },
33
+ {
34
+ name: 'agent_crm_identity_remove',
35
+ description: 'Unlink a CRM login from its agent (id from agent_crm_identity_list).',
36
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'CRM login link id.' } }, required: ['id'] },
37
+ },
38
+ {
39
+ name: 'agent_crm_identity_list_unlinked',
40
+ description: 'List CRM logins that opened the Velaro call pop inside your CRM but are linked to no agent (they never see a call until linked).',
41
+ inputSchema: { type: 'object', properties: {} },
42
+ },
43
+ {
44
+ name: 'agent_crm_identity_link',
45
+ description: 'Link one unlinked CRM login (id from agent_crm_identity_list_unlinked) to a Velaro agent.',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: { id: { type: 'number', description: 'Unlinked CRM login id.' }, ...agentProps },
49
+ required: ['id'],
50
+ },
51
+ },
52
+ ];
53
+
54
+ function agentBody(a) {
55
+ if (!a.workspaceUserId && !a.agentEmail) throw new Error('Pass workspaceUserId or agentEmail.');
56
+ return { workspaceUserId: a.workspaceUserId ?? null, agentEmail: a.agentEmail ?? null };
57
+ }
58
+
59
+ export const handlers = {
60
+ agent_crm_identity_list: async (a, ctx) => {
61
+ const q = a.workspaceUserId ? `?workspaceUserId=${encodeURIComponent(a.workspaceUserId)}` : '';
62
+ const r = await ctx.messagingGet(`/api/v1/AgentCrmIdentities${q}`);
63
+ return { identities: r?.data?.identities ?? [] };
64
+ },
65
+ agent_crm_identity_add: async (a, ctx) => {
66
+ if (!a.loginEmail && !a.crmUserId) throw new Error('Pass loginEmail, crmUserId, or both.');
67
+ const r = await ctx.messagingPost('/api/v1/AgentCrmIdentities', {
68
+ ...agentBody(a),
69
+ provider: a.provider,
70
+ loginEmail: a.loginEmail ?? null,
71
+ crmUserId: a.crmUserId ?? null,
72
+ });
73
+ return r?.data ?? r;
74
+ },
75
+ agent_crm_identity_remove: async (a, ctx) => {
76
+ await ctx.messagingDel(`/api/v1/AgentCrmIdentities/${encodeURIComponent(a.id)}`);
77
+ return { removed: true, id: a.id };
78
+ },
79
+ agent_crm_identity_list_unlinked: async (_a, ctx) => {
80
+ const r = await ctx.messagingGet('/api/v1/AgentCrmIdentities/unlinked-logins');
81
+ return { logins: r?.data?.logins ?? [] };
82
+ },
83
+ agent_crm_identity_link: async (a, ctx) => {
84
+ const r = await ctx.messagingPost(`/api/v1/AgentCrmIdentities/unlinked-logins/${encodeURIComponent(a.id)}/link`, agentBody(a));
85
+ return r?.data ?? r;
86
+ },
87
+ };
@@ -0,0 +1,40 @@
1
+ // velaro billing-insights (customer): billing page recommendations and the capacity-button / recommendations preferences.
2
+ // Parity module (CLI group -> MCP tools). Site comes from the credential; only a site administrator sees or changes these.
3
+
4
+ const pid = (v, n = 'id') => { const x = Number(v); if (!Number.isInteger(x) || x <= 0) throw new Error(n + ' must be a positive integer'); return x; };
5
+
6
+ export const tools = [
7
+ {
8
+ name: 'billing_insights_list_recommendations',
9
+ description: 'List this month\'s billing recommendations for your site (usage that fits a larger plan, included features that are switched off, add-ons that match how you use Velaro). Quiet by default: at most 3 a month. Site administrators only.',
10
+ inputSchema: {"type":"object","properties":{}},
11
+ },
12
+ {
13
+ name: 'billing_insights_dismiss_recommendation',
14
+ description: 'Dismiss a billing recommendation. It stays hidden until the numbers behind it change.',
15
+ inputSchema: {"type":"object","properties":{"id":{"type":"integer","description":"Recommendation id from billing_insights_list_recommendations"}},"required":["id"]},
16
+ },
17
+ {
18
+ name: 'billing_insights_get_preferences',
19
+ description: 'Show whether the Add capacity / Compare plans buttons are hidden and whether recommendations are on for your site.',
20
+ inputSchema: {"type":"object","properties":{}},
21
+ },
22
+ {
23
+ name: 'billing_insights_set_preferences',
24
+ description: 'Hide or show the Add capacity / Compare plans buttons, and turn billing recommendations on or off for your whole site. Site administrators only.',
25
+ inputSchema: {"type":"object","properties":{"hideCapacityButtons":{"type":"boolean","description":"Hide the Add capacity and Compare plans buttons"},"recommendationsEnabled":{"type":"boolean","description":"Turn billing recommendations on or off"}}},
26
+ },
27
+ ];
28
+
29
+ export const handlers = {
30
+ billing_insights_list_recommendations: async (_a, ctx) => ctx.api('GET', '/BillingInsights/recommendations'),
31
+ billing_insights_dismiss_recommendation: async (a, ctx) => ctx.api('POST', '/BillingInsights/recommendations/' + pid(a.id) + '/dismiss', {}),
32
+ billing_insights_get_preferences: async (_a, ctx) => ctx.api('GET', '/BillingInsights/preferences'),
33
+ billing_insights_set_preferences: async (a, ctx) => {
34
+ const body = {};
35
+ if (a.hideCapacityButtons !== undefined) body.hideCapacityButtons = a.hideCapacityButtons === true || a.hideCapacityButtons === 'true';
36
+ if (a.recommendationsEnabled !== undefined) body.recommendationsEnabled = a.recommendationsEnabled === true || a.recommendationsEnabled === 'true';
37
+ if (Object.keys(body).length === 0) throw new Error('Provide hideCapacityButtons and/or recommendationsEnabled');
38
+ return ctx.api('PUT', '/BillingInsights/preferences', body);
39
+ },
40
+ };
@@ -0,0 +1,43 @@
1
+ // velaro authority get|set|history|revert: which system the AI uses when two connected systems overlap
2
+ // (capability authority, today realtime_stock). Site comes from the credential. Parity module (CLI group -> MCP tools).
3
+
4
+ const enc = encodeURIComponent;
5
+ const cap = (a) => {
6
+ const c = String(a.capability ?? '').trim();
7
+ if (!/^[a-z][a-z0-9_]*$/.test(c)) throw new Error('capability must be a snake_case name such as realtime_stock');
8
+ return enc(c);
9
+ };
10
+
11
+ const CAP = { capability: { type: 'string', description: 'Capability name, e.g. realtime_stock' } };
12
+
13
+ export const tools = [
14
+ {
15
+ name: 'capability_authority_get',
16
+ description: 'List capability authority rules (which system wins when two overlap, e.g. ERP stock over storefront stock) with the current mode for this site, plus the last 10 changes for the named capability when capability is given.',
17
+ inputSchema: {"type":"object","properties":{...CAP}},
18
+ },
19
+ {
20
+ name: 'capability_authority_set',
21
+ description: 'Set a capability authority mode: "default" (Velaro rule applies) or "off" (keep both tools for this site). Administrator only; logged and revertable.',
22
+ inputSchema: {"type":"object","properties":{...CAP,"mode":{"type":"string","enum":["default","off"]}},"required":["capability","mode"]},
23
+ },
24
+ {
25
+ name: 'capability_authority_revert',
26
+ description: 'Revert the last change to a capability authority mode. Administrator only; errors (409) when there is nothing to revert.',
27
+ inputSchema: {"type":"object","properties":{...CAP},"required":["capability"]},
28
+ },
29
+ ];
30
+
31
+ export const handlers = {
32
+ capability_authority_get: async (a, ctx) => {
33
+ const list = await ctx.messagingGet('/CapabilityAuthority');
34
+ if (!a.capability) return list;
35
+ const history = await ctx.messagingGet('/CapabilityAuthority/' + cap(a) + '/History');
36
+ return { ...list, history: history?.history ?? [] };
37
+ },
38
+ capability_authority_set: async (a, ctx) => {
39
+ if (a.mode !== 'default' && a.mode !== 'off') throw new Error('mode must be "default" or "off"');
40
+ return ctx.messagingPut('/CapabilityAuthority/' + cap(a), { mode: a.mode });
41
+ },
42
+ capability_authority_revert: async (a, ctx) => ctx.messagingPost('/CapabilityAuthority/' + cap(a) + '/Revert', {}),
43
+ };
@@ -0,0 +1,38 @@
1
+ // MCP parity for `velaro commerce-routing` (cli/lib/commands/commerce-routing.js). The site comes from the credential only (tenant isolation).
2
+ const CAPABILITIES = ['product_search', 'order_status', 'stock_check', 'customer_lookup'];
3
+ const SYSTEMS = ['magento', 'shopify', 'bigcommerce', 'woocommerce', 'netsuite'];
4
+
5
+ export const tools = [
6
+ {
7
+ name: 'commerce_routing_get',
8
+ description: 'Show which connected commerce system the AI asks first for each kind of question (product questions, order status, stock, customer lookups) when the site has more than one connected, with the default and any override. Lives in velaro-messaging (GET /CommerceRouting).',
9
+ inputSchema: { type: 'object', properties: {} },
10
+ },
11
+ {
12
+ name: 'commerce_routing_set',
13
+ description: 'Set or clear the per-capability first-choice commerce system for this site. Capabilities left out are cleared back to the default; an empty preferred object clears everything. Site administrators only. Lives in velaro-messaging (PUT /CommerceRouting).',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ preferred: {
18
+ type: 'object',
19
+ description: `Map of capability (${CAPABILITIES.join(', ')}) to system (${SYSTEMS.join(', ')}).`,
20
+ additionalProperties: { type: 'string', enum: SYSTEMS },
21
+ },
22
+ },
23
+ required: ['preferred'],
24
+ },
25
+ },
26
+ ];
27
+
28
+ export const handlers = {
29
+ commerce_routing_get: async (_a, ctx) => ctx.messagingGet('/CommerceRouting'),
30
+ commerce_routing_set: async (a, ctx) => {
31
+ const preferred = a?.preferred ?? {};
32
+ for (const [cap, sys] of Object.entries(preferred)) {
33
+ if (!CAPABILITIES.includes(cap)) throw new Error(`Unknown capability '${cap}'. Use one of: ${CAPABILITIES.join(', ')}`);
34
+ if (!SYSTEMS.includes(String(sys).toLowerCase())) throw new Error(`Unknown system '${sys}'. Use one of: ${SYSTEMS.join(', ')}`);
35
+ }
36
+ return ctx.messagingPut('/CommerceRouting', { preferred });
37
+ },
38
+ };
@@ -0,0 +1,107 @@
1
+ // MCP parity for `velaro site-sale` (cli/lib/commands/site-sale.js): Current Sales (time-boxed bot override Q&A +
2
+ // per-customer coupons). Site comes from the credential, never an argument.
3
+ export const tools = [
4
+ {
5
+ name: 'site_sale_list',
6
+ description: "List this site's Current Sales (temporary sales or discounts the bot answers about deterministically until they end).",
7
+ inputSchema: { type: 'object', properties: {}, required: [] },
8
+ },
9
+ {
10
+ name: 'site_sale_create',
11
+ description: 'Create a Current Sale: while active, the bot answers sales, discount and coupon questions from this definition and can issue per-customer coupons (Magento sites mint a real cart price rule). It stops at the end time. Times are UTC; convert from Pacific.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ name: { type: 'string', description: 'Sale name shown to customers' },
16
+ description: { type: 'string', description: 'Short description the bot can quote' },
17
+ appliesTo: { type: 'string', enum: ['all','category','products'], description: 'Scope of the sale' },
18
+ appliesToValue: { type: 'string', description: 'Category name or comma-separated product identifiers when appliesTo is category or products' },
19
+ discountType: { type: 'string', enum: ['percentage','fixed_amount','free_shipping'], description: 'Kind of discount' },
20
+ discountValue: { type: 'number', description: 'Percent or fixed amount (ignored for free_shipping)' },
21
+ startsAtUtc: { type: 'string', description: 'ISO 8601 start time in UTC (convert from Pacific)' },
22
+ endsAtUtc: { type: 'string', description: 'ISO 8601 end time in UTC; the sale stops answering at this time' },
23
+ couponCode: { type: 'string', description: 'Optional shared coupon code' },
24
+ autoGenerateCoupons: { type: 'boolean', description: 'Let the bot issue a unique coupon per customer' },
25
+ maxCouponsPerCustomer: { type: 'number', description: 'Max coupons the bot issues to one customer' },
26
+ couponUsageLimit: { type: 'number', description: 'Times each coupon can be used' },
27
+ couponExpiresHours: { type: 'number', description: 'Hours before an issued coupon expires' },
28
+ },
29
+ required: ['name', 'discountType', 'startsAtUtc', 'endsAtUtc'],
30
+ },
31
+ },
32
+ {
33
+ name: 'site_sale_update',
34
+ description: 'Update a Current Sale by id. Omit a field to leave it unchanged.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ id: { type: 'number', description: 'Sale ID from site_sale_list. Required.' },
39
+ name: { type: 'string', description: 'Sale name shown to customers' },
40
+ description: { type: 'string', description: 'Short description the bot can quote' },
41
+ appliesTo: { type: 'string', enum: ['all','category','products'], description: 'Scope of the sale' },
42
+ appliesToValue: { type: 'string', description: 'Category name or comma-separated product identifiers when appliesTo is category or products' },
43
+ discountType: { type: 'string', enum: ['percentage','fixed_amount','free_shipping'], description: 'Kind of discount' },
44
+ discountValue: { type: 'number', description: 'Percent or fixed amount (ignored for free_shipping)' },
45
+ startsAtUtc: { type: 'string', description: 'ISO 8601 start time in UTC (convert from Pacific)' },
46
+ endsAtUtc: { type: 'string', description: 'ISO 8601 end time in UTC; the sale stops answering at this time' },
47
+ couponCode: { type: 'string', description: 'Optional shared coupon code' },
48
+ autoGenerateCoupons: { type: 'boolean', description: 'Let the bot issue a unique coupon per customer' },
49
+ maxCouponsPerCustomer: { type: 'number', description: 'Max coupons the bot issues to one customer' },
50
+ couponUsageLimit: { type: 'number', description: 'Times each coupon can be used' },
51
+ couponExpiresHours: { type: 'number', description: 'Hours before an issued coupon expires' },
52
+ },
53
+ required: ['id'],
54
+ },
55
+ },
56
+ {
57
+ name: 'site_sale_end',
58
+ description: 'End a Current Sale now. The bot stops answering from it immediately.',
59
+ inputSchema: {
60
+ type: 'object',
61
+ properties: { id: { type: 'number', description: 'Sale ID. Required.' } },
62
+ required: ['id'],
63
+ },
64
+ },
65
+ {
66
+ name: 'site_sale_delete',
67
+ description: 'Delete a Current Sale by id.',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: { id: { type: 'number', description: 'Sale ID. Required.' } },
71
+ required: ['id'],
72
+ },
73
+ },
74
+ ];
75
+
76
+ // Field names must match the SiteSales request on the messaging API exactly (undefined is dropped by JSON).
77
+ const saleBody = (a) => ({
78
+ name: a.name,
79
+ description: a.description,
80
+ appliesTo: a.appliesTo,
81
+ appliesToValue: a.appliesToValue,
82
+ discountType: a.discountType,
83
+ discountValue: a.discountValue,
84
+ startsAtUtc: a.startsAtUtc,
85
+ endsAtUtc: a.endsAtUtc,
86
+ couponCode: a.couponCode,
87
+ autoGenerateCoupons: a.autoGenerateCoupons,
88
+ maxCouponsPerCustomer: a.maxCouponsPerCustomer,
89
+ couponUsageLimit: a.couponUsageLimit,
90
+ couponExpiresHours: a.couponExpiresHours,
91
+ });
92
+
93
+ const saleId = (a) => {
94
+ if (a.id == null) throw new Error('id is required.');
95
+ return encodeURIComponent(a.id);
96
+ };
97
+
98
+ export const handlers = {
99
+ site_sale_list: async (_a, ctx) => ctx.messagingGet('/SiteSales'),
100
+ site_sale_create: async (a, ctx) => {
101
+ if (!a.name || !a.discountType || !a.startsAtUtc || !a.endsAtUtc) throw new Error('name, discountType, startsAtUtc and endsAtUtc are required.');
102
+ return ctx.messagingPost('/SiteSales', saleBody(a));
103
+ },
104
+ site_sale_update: async (a, ctx) => ctx.messagingPut(`/SiteSales/${saleId(a)}`, saleBody(a)),
105
+ site_sale_end: async (a, ctx) => ctx.messagingPost(`/SiteSales/${saleId(a)}/end`, {}),
106
+ site_sale_delete: async (a, ctx) => ctx.messagingDel(`/SiteSales/${saleId(a)}`),
107
+ };
@@ -0,0 +1,78 @@
1
+ // MCP parity for `velaro sms tollfree` (cli/lib/commands/sms.js). Site from the credential only.
2
+ // Backend: velaro-messaging Sms/TollFree (SmsTollFreeController -> TollFreeSmsService). Text-enables a toll-free number the
3
+ // site already owns (for example an 833 line whose calls run on Microsoft Teams Phone): Twilio hosts the SMS side only.
4
+ const address = {
5
+ street: { type: 'string' },
6
+ city: { type: 'string' },
7
+ region: { type: 'string', description: 'State or province' },
8
+ postalCode: { type: 'string' },
9
+ country: { type: 'string', description: 'Two-letter country (default US)' },
10
+ };
11
+
12
+ export const tools = [
13
+ {
14
+ name: 'sms_tollfree_status',
15
+ description: 'Status of texting on one of your toll-free numbers: hosting order, text-enabled, in your Inbox, carrier verification, and whether it can send texts. When hosting has completed this also adds the Inbox channel.',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ phoneNumber: { type: 'string', description: 'E.164, e.g. +18335550100' },
20
+ teamId: { type: 'number', description: 'Team whose Inbox receives the texts (default first team).' },
21
+ },
22
+ required: ['phoneNumber'],
23
+ },
24
+ },
25
+ {
26
+ name: 'sms_tollfree_host',
27
+ description: 'Request texting on a toll-free number you already own (calls stay with your phone system). Twilio emails the contact a letter of authorization to sign.',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ phoneNumber: { type: 'string' },
32
+ teamId: { type: 'number' },
33
+ businessName: { type: 'string' },
34
+ contactName: { type: 'string' },
35
+ contactEmail: { type: 'string' },
36
+ contactPhone: { type: 'string' },
37
+ ...address,
38
+ },
39
+ required: ['phoneNumber', 'businessName', 'contactEmail', 'contactPhone', 'street', 'city', 'region', 'postalCode'],
40
+ },
41
+ },
42
+ {
43
+ name: 'sms_tollfree_verify',
44
+ description: 'Submit the carrier toll-free verification for a text-enabled toll-free number (required before US carriers deliver its texts).',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ phoneNumber: { type: 'string' },
49
+ businessName: { type: 'string' },
50
+ businessWebsite: { type: 'string' },
51
+ notificationEmail: { type: 'string' },
52
+ useCaseCategories: { type: 'array', items: { type: 'string' }, description: 'e.g. CUSTOMER_CARE, ACCOUNT_NOTIFICATIONS' },
53
+ useCaseSummary: { type: 'string' },
54
+ productionMessageSample: { type: 'string' },
55
+ optInType: { type: 'string', enum: ['VERBAL', 'WEB_FORM', 'PAPER_FORM', 'VIA_TEXT', 'MOBILE_QR_CODE'] },
56
+ optInImageUrls: { type: 'array', items: { type: 'string' } },
57
+ messageVolume: { type: 'string', description: 'e.g. 1,000' },
58
+ contactFirstName: { type: 'string' },
59
+ contactLastName: { type: 'string' },
60
+ contactEmail: { type: 'string' },
61
+ contactPhone: { type: 'string' },
62
+ additionalInformation: { type: 'string' },
63
+ ...address,
64
+ },
65
+ required: ['phoneNumber'],
66
+ },
67
+ },
68
+ ];
69
+
70
+ export const handlers = {
71
+ sms_tollfree_status: async (a, ctx) => {
72
+ if (!a.phoneNumber) throw new Error('phoneNumber is required.');
73
+ const team = a.teamId ? '&teamId=' + encodeURIComponent(a.teamId) : '';
74
+ return ctx.messagingGet('/Sms/TollFree/Status?phoneNumber=' + encodeURIComponent(a.phoneNumber) + team);
75
+ },
76
+ sms_tollfree_host: async (a, ctx) => ctx.messagingPost('/Sms/TollFree/Host', a),
77
+ sms_tollfree_verify: async (a, ctx) => ctx.messagingPost('/Sms/TollFree/Verification', a),
78
+ };
package/server.js CHANGED
@@ -967,6 +967,7 @@ Key node shapes:
967
967
  • transferToAI: { type:"action", actionType:"transferToAI", aiConfigId:N }
968
968
  • selectPreChat: { type:"action", actionType:"selectPreChat", surveyId:N, surveyCompact:true|false|null, child:N }
969
969
  surveyCompact (optional tri-state): null/omitted = follow the survey's own compactMode, true = compact, false = regular.
970
+ • redirectToUrl: { type:"action", actionType:"redirectToUrl", redirectUrl:"https://host/path" | "//host/path", child:N } (chat_button_clicked only: browser goes to the page instead of opening chat; http/https only; use on an Unavailable branch)
970
971
  • sendSurvey: { type:"action", actionType:"sendSurvey", sendSurveyId:N, surveyDeliveryMode:"conversational"|"form", closeAfterSurvey:true|false, child:N }
971
972
  closeAfterSurvey (optional, default false): when true the chat closes once the visitor completes the survey (use for Unavailable/after-hours surveys that replace a live chat). Omit to keep the old behaviour.
972
973
  AI follow-up suggestions ("AI mode"): add suggestFollowUps:true|false (omit = use the account default, set via ai_followups_site_set), suggestFollowUpCount:2-4, suggestFollowUpInstruction:"<=300 chars". After each web-chat bot reply the bot offers short tappable follow-up replies; typing stays available.
@@ -2407,6 +2408,17 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2407
2408
  required: [],
2408
2409
  },
2409
2410
  },
2411
+ {
2412
+ name: 'outbound_activity_volume',
2413
+ description: 'Daily outbound email volume for this account over the last N days (1-90, default 30), split sent vs failed, for campaign (sequence) and broadcast sends. Transactional mail is reported as NOT COUNTED until messaging exposes a daily aggregate. Site scoping is enforced server-side.',
2414
+ inputSchema: {
2415
+ type: 'object',
2416
+ properties: {
2417
+ days: { type: 'number', description: 'Window in days, 1-90 (default 30)' },
2418
+ },
2419
+ required: [],
2420
+ },
2421
+ },
2410
2422
  {
2411
2423
  name: 'outbound_activity_for_contact',
2412
2424
  description: 'Every email this account has sent to ONE specific address, across transactional, sequence and broadcast sends — the "what have we sent this customer" answer that previously required checking three separate screens. The email address is REQUIRED and matched exactly; it is never optional, because an omitted address must not widen this into a site-wide listing.',
@@ -4063,77 +4075,6 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
4063
4075
  required: ['contactId'],
4064
4076
  },
4065
4077
  },
4066
- // -- Current Sales (time-boxed bot override Q&A + per-customer coupons). Site comes from the credential, never an arg. --
4067
- {
4068
- name: 'site_sale_list',
4069
- description: "List this site's Current Sales (temporary sales or discounts the bot answers about deterministically until they end).",
4070
- inputSchema: { type: 'object', properties: {}, required: [] },
4071
- },
4072
- {
4073
- name: 'site_sale_create',
4074
- description: 'Create a Current Sale: while active, the bot answers sales, discount and coupon questions from this definition and can issue per-customer coupons (Magento sites mint a real cart price rule). It stops at the end time. Times are UTC; convert from Pacific.',
4075
- inputSchema: {
4076
- type: 'object',
4077
- properties: {
4078
- name: { type: 'string', description: 'Sale name shown to customers' },
4079
- description: { type: 'string', description: 'Short description the bot can quote' },
4080
- appliesTo: { type: 'string', enum: ['all','category','products'], description: 'Scope of the sale' },
4081
- appliesToValue: { type: 'string', description: 'Category name or comma-separated product identifiers when appliesTo is category or products' },
4082
- discountType: { type: 'string', enum: ['percentage','fixed_amount','free_shipping'], description: 'Kind of discount' },
4083
- discountValue: { type: 'number', description: 'Percent or fixed amount (ignored for free_shipping)' },
4084
- startsAtUtc: { type: 'string', description: 'ISO 8601 start time in UTC (convert from Pacific)' },
4085
- endsAtUtc: { type: 'string', description: 'ISO 8601 end time in UTC; the sale stops answering at this time' },
4086
- couponCode: { type: 'string', description: 'Optional shared coupon code' },
4087
- autoGenerateCoupons: { type: 'boolean', description: 'Let the bot issue a unique coupon per customer' },
4088
- maxCouponsPerCustomer: { type: 'number', description: 'Max coupons the bot issues to one customer' },
4089
- couponUsageLimit: { type: 'number', description: 'Times each coupon can be used' },
4090
- couponExpiresHours: { type: 'number', description: 'Hours before an issued coupon expires' },
4091
- },
4092
- required: ['name', 'discountType', 'startsAtUtc', 'endsAtUtc'],
4093
- },
4094
- },
4095
- {
4096
- name: 'site_sale_update',
4097
- description: 'Update a Current Sale by id. Omit a field to leave it unchanged.',
4098
- inputSchema: {
4099
- type: 'object',
4100
- properties: {
4101
- id: { type: 'number', description: 'Sale ID from site_sale_list. Required.' },
4102
- name: { type: 'string', description: 'Sale name shown to customers' },
4103
- description: { type: 'string', description: 'Short description the bot can quote' },
4104
- appliesTo: { type: 'string', enum: ['all','category','products'], description: 'Scope of the sale' },
4105
- appliesToValue: { type: 'string', description: 'Category name or comma-separated product identifiers when appliesTo is category or products' },
4106
- discountType: { type: 'string', enum: ['percentage','fixed_amount','free_shipping'], description: 'Kind of discount' },
4107
- discountValue: { type: 'number', description: 'Percent or fixed amount (ignored for free_shipping)' },
4108
- startsAtUtc: { type: 'string', description: 'ISO 8601 start time in UTC (convert from Pacific)' },
4109
- endsAtUtc: { type: 'string', description: 'ISO 8601 end time in UTC; the sale stops answering at this time' },
4110
- couponCode: { type: 'string', description: 'Optional shared coupon code' },
4111
- autoGenerateCoupons: { type: 'boolean', description: 'Let the bot issue a unique coupon per customer' },
4112
- maxCouponsPerCustomer: { type: 'number', description: 'Max coupons the bot issues to one customer' },
4113
- couponUsageLimit: { type: 'number', description: 'Times each coupon can be used' },
4114
- couponExpiresHours: { type: 'number', description: 'Hours before an issued coupon expires' },
4115
- },
4116
- required: ['id'],
4117
- },
4118
- },
4119
- {
4120
- name: 'site_sale_end',
4121
- description: 'End a Current Sale now. The bot stops answering from it immediately.',
4122
- inputSchema: {
4123
- type: 'object',
4124
- properties: { id: { type: 'number', description: 'Sale ID. Required.' } },
4125
- required: ['id'],
4126
- },
4127
- },
4128
- {
4129
- name: 'site_sale_delete',
4130
- description: 'Delete a Current Sale by id.',
4131
- inputSchema: {
4132
- type: 'object',
4133
- properties: { id: { type: 'number', description: 'Sale ID. Required.' } },
4134
- required: ['id'],
4135
- },
4136
- },
4137
4078
  {
4138
4079
  name: 'pickup_get_config',
4139
4080
  description: 'Get this site\'s Pickup & Reminder Texting configuration (default cadence, max reminders, message template, enabled state, quiet-hours override). Returns null/404 if no config has been created yet -- use pickup_update_config to create one.',
@@ -7070,6 +7011,28 @@ reply: ${r?.reply}`;
7070
7011
  return JSON.stringify(r, null, 2);
7071
7012
  }
7072
7013
 
7014
+ // ── Unified Outbound Activity: daily volume ──────────────────────────
7015
+ case 'outbound_activity_volume': {
7016
+ const days = Math.min(90, Math.max(1, Number(args.days ?? 30)));
7017
+ const v = await api('GET', `/Marketing/OutboundActivity/Volume?days=${days}`);
7018
+ const byDay = new Map();
7019
+ for (const b of v?.buckets ?? []) {
7020
+ const d = byDay.get(b.date) ?? { sent: 0, failed: 0 };
7021
+ if (b.status === 'Failed') d.failed += b.count;
7022
+ else if (b.status === 'Sent') d.sent += b.count;
7023
+ byDay.set(b.date, d);
7024
+ }
7025
+ const lines = [`=== Outbound volume, last ${days} days (campaign + broadcast) ===`];
7026
+ if (v?.degradedSources?.length) {
7027
+ lines.push(` ! NOT COUNTED: ${v.degradedSources.join(', ')}`);
7028
+ }
7029
+ for (const [date, d] of [...byDay.entries()].sort(([a], [b]) => a.localeCompare(b))) {
7030
+ lines.push(` ${date} sent ${d.sent} failed ${d.failed}`);
7031
+ }
7032
+ if (!byDay.size) lines.push(' No campaign or broadcast sends in this window.');
7033
+ return lines.join('\n');
7034
+ }
7035
+
7073
7036
  // ── Unified Outbound Activity ──────────────────────────────────────────
7074
7037
  case 'outbound_activity_list':
7075
7038
  case 'outbound_activity_for_contact': {
@@ -8412,66 +8375,6 @@ ${JSON.stringify(result, null, 2)}`;
8412
8375
  return JSON.stringify(history, null, 2);
8413
8376
  }
8414
8377
 
8415
- case 'site_sale_list': {
8416
- const result = await messagingGet('/SiteSales');
8417
- return JSON.stringify(result, null, 2);
8418
- }
8419
-
8420
- case 'site_sale_create': {
8421
- if (!args.name || !args.discountType || !args.startsAtUtc || !args.endsAtUtc) return 'name, discountType, startsAtUtc and endsAtUtc are required.';
8422
- // Field names must match the SiteSales request on the messaging API exactly (undefined is dropped by JSON).
8423
- const body = {
8424
- name: args.name,
8425
- description: args.description,
8426
- appliesTo: args.appliesTo,
8427
- appliesToValue: args.appliesToValue,
8428
- discountType: args.discountType,
8429
- discountValue: args.discountValue,
8430
- startsAtUtc: args.startsAtUtc,
8431
- endsAtUtc: args.endsAtUtc,
8432
- couponCode: args.couponCode,
8433
- autoGenerateCoupons: args.autoGenerateCoupons,
8434
- maxCouponsPerCustomer: args.maxCouponsPerCustomer,
8435
- couponUsageLimit: args.couponUsageLimit,
8436
- couponExpiresHours: args.couponExpiresHours,
8437
- };
8438
- const result = await messagingPost('/SiteSales', body);
8439
- return JSON.stringify(result, null, 2);
8440
- }
8441
-
8442
- case 'site_sale_update': {
8443
- if (args.id == null) return 'id is required.';
8444
- const body = {
8445
- name: args.name,
8446
- description: args.description,
8447
- appliesTo: args.appliesTo,
8448
- appliesToValue: args.appliesToValue,
8449
- discountType: args.discountType,
8450
- discountValue: args.discountValue,
8451
- startsAtUtc: args.startsAtUtc,
8452
- endsAtUtc: args.endsAtUtc,
8453
- couponCode: args.couponCode,
8454
- autoGenerateCoupons: args.autoGenerateCoupons,
8455
- maxCouponsPerCustomer: args.maxCouponsPerCustomer,
8456
- couponUsageLimit: args.couponUsageLimit,
8457
- couponExpiresHours: args.couponExpiresHours,
8458
- };
8459
- const result = await messagingPut(`/SiteSales/${encodeURIComponent(args.id)}`, body);
8460
- return JSON.stringify(result, null, 2);
8461
- }
8462
-
8463
- case 'site_sale_end': {
8464
- if (args.id == null) return 'id is required.';
8465
- const result = await messagingPost(`/SiteSales/${encodeURIComponent(args.id)}/end`, {});
8466
- return JSON.stringify(result, null, 2);
8467
- }
8468
-
8469
- case 'site_sale_delete': {
8470
- if (args.id == null) return 'id is required.';
8471
- const result = await messagingDel(`/SiteSales/${encodeURIComponent(args.id)}`);
8472
- return JSON.stringify(result, null, 2);
8473
- }
8474
-
8475
8378
  case 'pickup_get_config': {
8476
8379
  const config = await messagingGet('/PickupNotifications/Config').catch(err => {
8477
8380
  if (String(err.message).includes(' 404')) return null;
@@ -25,6 +25,7 @@ const MUTATING = new Set([
25
25
  'routing_team_update_config', 'pickup_update_config', 'payment_recovery_update_config',
26
26
  'recruiting_sync_jobs', 'ai_config_set_reply_format', 'magento_set_sidebar_fields',
27
27
  'zoom_phone_set_config',
28
+ 'billing_insights_set_preferences', 'billing_insights_dismiss_recommendation',
28
29
  'site_sale_create', 'site_sale_update', 'site_sale_end',
29
30
  ]);
30
31
 
@@ -57,7 +58,7 @@ const READ_ONLY_EXPLICIT = new Set([
57
58
  // Write tools that are additive even though their name carries a destructive-looking token.
58
59
  const NON_DESTRUCTIVE_EXPLICIT = new Set([
59
60
  'safety_settings_test', 'safety_need_help', 'canned_learning_record_edit', 'netsuite_suiteapp_download',
60
- 'chat_topics_reclassify',
61
+ 'chat_topics_reclassify', 'agent_crm_identity_link',
61
62
  ]);
62
63
 
63
64
  const DESTRUCTIVE_TOKENS = new Set([