@velaro/mcp-server 2.0.2 → 2.0.4
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 +1 -1
- package/parity/agent-crm-identity.customer.js +87 -0
- package/server.js +165 -0
- package/tool-annotations.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velaro/mcp-server",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
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
|
+
};
|
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,6 +4075,77 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
4063
4075
|
required: ['contactId'],
|
|
4064
4076
|
},
|
|
4065
4077
|
},
|
|
4078
|
+
// -- Current Sales (time-boxed bot override Q&A + per-customer coupons). Site comes from the credential, never an arg. --
|
|
4079
|
+
{
|
|
4080
|
+
name: 'site_sale_list',
|
|
4081
|
+
description: "List this site's Current Sales (temporary sales or discounts the bot answers about deterministically until they end).",
|
|
4082
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
4083
|
+
},
|
|
4084
|
+
{
|
|
4085
|
+
name: 'site_sale_create',
|
|
4086
|
+
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.',
|
|
4087
|
+
inputSchema: {
|
|
4088
|
+
type: 'object',
|
|
4089
|
+
properties: {
|
|
4090
|
+
name: { type: 'string', description: 'Sale name shown to customers' },
|
|
4091
|
+
description: { type: 'string', description: 'Short description the bot can quote' },
|
|
4092
|
+
appliesTo: { type: 'string', enum: ['all','category','products'], description: 'Scope of the sale' },
|
|
4093
|
+
appliesToValue: { type: 'string', description: 'Category name or comma-separated product identifiers when appliesTo is category or products' },
|
|
4094
|
+
discountType: { type: 'string', enum: ['percentage','fixed_amount','free_shipping'], description: 'Kind of discount' },
|
|
4095
|
+
discountValue: { type: 'number', description: 'Percent or fixed amount (ignored for free_shipping)' },
|
|
4096
|
+
startsAtUtc: { type: 'string', description: 'ISO 8601 start time in UTC (convert from Pacific)' },
|
|
4097
|
+
endsAtUtc: { type: 'string', description: 'ISO 8601 end time in UTC; the sale stops answering at this time' },
|
|
4098
|
+
couponCode: { type: 'string', description: 'Optional shared coupon code' },
|
|
4099
|
+
autoGenerateCoupons: { type: 'boolean', description: 'Let the bot issue a unique coupon per customer' },
|
|
4100
|
+
maxCouponsPerCustomer: { type: 'number', description: 'Max coupons the bot issues to one customer' },
|
|
4101
|
+
couponUsageLimit: { type: 'number', description: 'Times each coupon can be used' },
|
|
4102
|
+
couponExpiresHours: { type: 'number', description: 'Hours before an issued coupon expires' },
|
|
4103
|
+
},
|
|
4104
|
+
required: ['name', 'discountType', 'startsAtUtc', 'endsAtUtc'],
|
|
4105
|
+
},
|
|
4106
|
+
},
|
|
4107
|
+
{
|
|
4108
|
+
name: 'site_sale_update',
|
|
4109
|
+
description: 'Update a Current Sale by id. Omit a field to leave it unchanged.',
|
|
4110
|
+
inputSchema: {
|
|
4111
|
+
type: 'object',
|
|
4112
|
+
properties: {
|
|
4113
|
+
id: { type: 'number', description: 'Sale ID from site_sale_list. Required.' },
|
|
4114
|
+
name: { type: 'string', description: 'Sale name shown to customers' },
|
|
4115
|
+
description: { type: 'string', description: 'Short description the bot can quote' },
|
|
4116
|
+
appliesTo: { type: 'string', enum: ['all','category','products'], description: 'Scope of the sale' },
|
|
4117
|
+
appliesToValue: { type: 'string', description: 'Category name or comma-separated product identifiers when appliesTo is category or products' },
|
|
4118
|
+
discountType: { type: 'string', enum: ['percentage','fixed_amount','free_shipping'], description: 'Kind of discount' },
|
|
4119
|
+
discountValue: { type: 'number', description: 'Percent or fixed amount (ignored for free_shipping)' },
|
|
4120
|
+
startsAtUtc: { type: 'string', description: 'ISO 8601 start time in UTC (convert from Pacific)' },
|
|
4121
|
+
endsAtUtc: { type: 'string', description: 'ISO 8601 end time in UTC; the sale stops answering at this time' },
|
|
4122
|
+
couponCode: { type: 'string', description: 'Optional shared coupon code' },
|
|
4123
|
+
autoGenerateCoupons: { type: 'boolean', description: 'Let the bot issue a unique coupon per customer' },
|
|
4124
|
+
maxCouponsPerCustomer: { type: 'number', description: 'Max coupons the bot issues to one customer' },
|
|
4125
|
+
couponUsageLimit: { type: 'number', description: 'Times each coupon can be used' },
|
|
4126
|
+
couponExpiresHours: { type: 'number', description: 'Hours before an issued coupon expires' },
|
|
4127
|
+
},
|
|
4128
|
+
required: ['id'],
|
|
4129
|
+
},
|
|
4130
|
+
},
|
|
4131
|
+
{
|
|
4132
|
+
name: 'site_sale_end',
|
|
4133
|
+
description: 'End a Current Sale now. The bot stops answering from it immediately.',
|
|
4134
|
+
inputSchema: {
|
|
4135
|
+
type: 'object',
|
|
4136
|
+
properties: { id: { type: 'number', description: 'Sale ID. Required.' } },
|
|
4137
|
+
required: ['id'],
|
|
4138
|
+
},
|
|
4139
|
+
},
|
|
4140
|
+
{
|
|
4141
|
+
name: 'site_sale_delete',
|
|
4142
|
+
description: 'Delete a Current Sale by id.',
|
|
4143
|
+
inputSchema: {
|
|
4144
|
+
type: 'object',
|
|
4145
|
+
properties: { id: { type: 'number', description: 'Sale ID. Required.' } },
|
|
4146
|
+
required: ['id'],
|
|
4147
|
+
},
|
|
4148
|
+
},
|
|
4066
4149
|
{
|
|
4067
4150
|
name: 'pickup_get_config',
|
|
4068
4151
|
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.',
|
|
@@ -6999,6 +7082,28 @@ reply: ${r?.reply}`;
|
|
|
6999
7082
|
return JSON.stringify(r, null, 2);
|
|
7000
7083
|
}
|
|
7001
7084
|
|
|
7085
|
+
// ── Unified Outbound Activity: daily volume ──────────────────────────
|
|
7086
|
+
case 'outbound_activity_volume': {
|
|
7087
|
+
const days = Math.min(90, Math.max(1, Number(args.days ?? 30)));
|
|
7088
|
+
const v = await api('GET', `/Marketing/OutboundActivity/Volume?days=${days}`);
|
|
7089
|
+
const byDay = new Map();
|
|
7090
|
+
for (const b of v?.buckets ?? []) {
|
|
7091
|
+
const d = byDay.get(b.date) ?? { sent: 0, failed: 0 };
|
|
7092
|
+
if (b.status === 'Failed') d.failed += b.count;
|
|
7093
|
+
else if (b.status === 'Sent') d.sent += b.count;
|
|
7094
|
+
byDay.set(b.date, d);
|
|
7095
|
+
}
|
|
7096
|
+
const lines = [`=== Outbound volume, last ${days} days (campaign + broadcast) ===`];
|
|
7097
|
+
if (v?.degradedSources?.length) {
|
|
7098
|
+
lines.push(` ! NOT COUNTED: ${v.degradedSources.join(', ')}`);
|
|
7099
|
+
}
|
|
7100
|
+
for (const [date, d] of [...byDay.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
7101
|
+
lines.push(` ${date} sent ${d.sent} failed ${d.failed}`);
|
|
7102
|
+
}
|
|
7103
|
+
if (!byDay.size) lines.push(' No campaign or broadcast sends in this window.');
|
|
7104
|
+
return lines.join('\n');
|
|
7105
|
+
}
|
|
7106
|
+
|
|
7002
7107
|
// ── Unified Outbound Activity ──────────────────────────────────────────
|
|
7003
7108
|
case 'outbound_activity_list':
|
|
7004
7109
|
case 'outbound_activity_for_contact': {
|
|
@@ -8341,6 +8446,66 @@ ${JSON.stringify(result, null, 2)}`;
|
|
|
8341
8446
|
return JSON.stringify(history, null, 2);
|
|
8342
8447
|
}
|
|
8343
8448
|
|
|
8449
|
+
case 'site_sale_list': {
|
|
8450
|
+
const result = await messagingGet('/SiteSales');
|
|
8451
|
+
return JSON.stringify(result, null, 2);
|
|
8452
|
+
}
|
|
8453
|
+
|
|
8454
|
+
case 'site_sale_create': {
|
|
8455
|
+
if (!args.name || !args.discountType || !args.startsAtUtc || !args.endsAtUtc) return 'name, discountType, startsAtUtc and endsAtUtc are required.';
|
|
8456
|
+
// Field names must match the SiteSales request on the messaging API exactly (undefined is dropped by JSON).
|
|
8457
|
+
const body = {
|
|
8458
|
+
name: args.name,
|
|
8459
|
+
description: args.description,
|
|
8460
|
+
appliesTo: args.appliesTo,
|
|
8461
|
+
appliesToValue: args.appliesToValue,
|
|
8462
|
+
discountType: args.discountType,
|
|
8463
|
+
discountValue: args.discountValue,
|
|
8464
|
+
startsAtUtc: args.startsAtUtc,
|
|
8465
|
+
endsAtUtc: args.endsAtUtc,
|
|
8466
|
+
couponCode: args.couponCode,
|
|
8467
|
+
autoGenerateCoupons: args.autoGenerateCoupons,
|
|
8468
|
+
maxCouponsPerCustomer: args.maxCouponsPerCustomer,
|
|
8469
|
+
couponUsageLimit: args.couponUsageLimit,
|
|
8470
|
+
couponExpiresHours: args.couponExpiresHours,
|
|
8471
|
+
};
|
|
8472
|
+
const result = await messagingPost('/SiteSales', body);
|
|
8473
|
+
return JSON.stringify(result, null, 2);
|
|
8474
|
+
}
|
|
8475
|
+
|
|
8476
|
+
case 'site_sale_update': {
|
|
8477
|
+
if (args.id == null) return 'id is required.';
|
|
8478
|
+
const body = {
|
|
8479
|
+
name: args.name,
|
|
8480
|
+
description: args.description,
|
|
8481
|
+
appliesTo: args.appliesTo,
|
|
8482
|
+
appliesToValue: args.appliesToValue,
|
|
8483
|
+
discountType: args.discountType,
|
|
8484
|
+
discountValue: args.discountValue,
|
|
8485
|
+
startsAtUtc: args.startsAtUtc,
|
|
8486
|
+
endsAtUtc: args.endsAtUtc,
|
|
8487
|
+
couponCode: args.couponCode,
|
|
8488
|
+
autoGenerateCoupons: args.autoGenerateCoupons,
|
|
8489
|
+
maxCouponsPerCustomer: args.maxCouponsPerCustomer,
|
|
8490
|
+
couponUsageLimit: args.couponUsageLimit,
|
|
8491
|
+
couponExpiresHours: args.couponExpiresHours,
|
|
8492
|
+
};
|
|
8493
|
+
const result = await messagingPut(`/SiteSales/${encodeURIComponent(args.id)}`, body);
|
|
8494
|
+
return JSON.stringify(result, null, 2);
|
|
8495
|
+
}
|
|
8496
|
+
|
|
8497
|
+
case 'site_sale_end': {
|
|
8498
|
+
if (args.id == null) return 'id is required.';
|
|
8499
|
+
const result = await messagingPost(`/SiteSales/${encodeURIComponent(args.id)}/end`, {});
|
|
8500
|
+
return JSON.stringify(result, null, 2);
|
|
8501
|
+
}
|
|
8502
|
+
|
|
8503
|
+
case 'site_sale_delete': {
|
|
8504
|
+
if (args.id == null) return 'id is required.';
|
|
8505
|
+
const result = await messagingDel(`/SiteSales/${encodeURIComponent(args.id)}`);
|
|
8506
|
+
return JSON.stringify(result, null, 2);
|
|
8507
|
+
}
|
|
8508
|
+
|
|
8344
8509
|
case 'pickup_get_config': {
|
|
8345
8510
|
const config = await messagingGet('/PickupNotifications/Config').catch(err => {
|
|
8346
8511
|
if (String(err.message).includes(' 404')) return null;
|
package/tool-annotations.js
CHANGED
|
@@ -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
|
+
'site_sale_create', 'site_sale_update', 'site_sale_end',
|
|
28
29
|
]);
|
|
29
30
|
|
|
30
31
|
// Names that look like writes but only read (checked before the write rules).
|
|
@@ -56,7 +57,7 @@ const READ_ONLY_EXPLICIT = new Set([
|
|
|
56
57
|
// Write tools that are additive even though their name carries a destructive-looking token.
|
|
57
58
|
const NON_DESTRUCTIVE_EXPLICIT = new Set([
|
|
58
59
|
'safety_settings_test', 'safety_need_help', 'canned_learning_record_edit', 'netsuite_suiteapp_download',
|
|
59
|
-
'chat_topics_reclassify',
|
|
60
|
+
'chat_topics_reclassify', 'agent_crm_identity_link',
|
|
60
61
|
]);
|
|
61
62
|
|
|
62
63
|
const DESTRUCTIVE_TOKENS = new Set([
|