@velaro/mcp-server 0.6.51 → 0.6.52
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/server.js +268 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velaro/mcp-server",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.52",
|
|
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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* VELARO_JWT=<token> short-lived Velaro JWT (from velaro login, for dev use)
|
|
10
10
|
*
|
|
11
11
|
* API base:
|
|
12
|
-
* VELARO_ADMIN_API=https://
|
|
12
|
+
* VELARO_ADMIN_API=https://api-admin-us-east.velaro.com (default, production)
|
|
13
13
|
* VELARO_ADMIN_API=https://velaro-admin-staging.azurewebsites.net (staging)
|
|
14
14
|
*
|
|
15
15
|
* Claude Desktop config (~/.claude/claude_desktop_config.json):
|
|
@@ -49,11 +49,45 @@ import {
|
|
|
49
49
|
ListPromptsRequestSchema,
|
|
50
50
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// Default was 'https://help.velaro.com'. Renamed to the recognizable admin API hostname for
|
|
53
|
+
// clarity during the 2026-09-10 Donaldson bot Calendly status investigation
|
|
54
|
+
// (docs/qa/chatbot-hallucinated-booking-donaldson-bot.md) — but this is NOT a functional fix and
|
|
55
|
+
// does NOT explain that incident: live-verified same day that 'https://help.velaro.com' and
|
|
56
|
+
// 'https://api-admin-us-east.velaro.com' are the SAME deployment (identical /api/health build SHA,
|
|
57
|
+
// and /Calendly/superadmin/status returns 401 — auth-gated, not 404 — on both hosts). Whatever
|
|
58
|
+
// caused that investigation's status/reality contradiction, it was NOT this default resolving to a
|
|
59
|
+
// broken or different host; that theory is disproven. Left as a naming clarity improvement only.
|
|
60
|
+
const API_BASE = process.env.VELARO_ADMIN_API || 'https://api-admin-us-east.velaro.com';
|
|
53
61
|
const MESSAGING_API = process.env.VELARO_MESSAGING_API || null;
|
|
54
62
|
const MCP_KEY = process.env.VELARO_MCP_KEY;
|
|
55
63
|
const JWT = process.env.VELARO_JWT;
|
|
56
64
|
|
|
65
|
+
// Known admin API hosts, for tools that let a caller explicitly target one environment
|
|
66
|
+
// regardless of what this server process's own API_BASE is configured to (see the `env` param
|
|
67
|
+
// on the calendly_* superadmin tools below). Deliberately NOT used as a default anywhere —
|
|
68
|
+
// defaulting env-selectable tools to one hardcoded environment while every other tool in this
|
|
69
|
+
// file follows the single global API_BASE would recreate the exact bug being fixed here: an
|
|
70
|
+
// operator configured for one environment would silently get a DIFFERENT environment's answer
|
|
71
|
+
// from just these tools with no indication anything was different.
|
|
72
|
+
const ADMIN_API_HOSTS = {
|
|
73
|
+
staging: 'https://velaro-admin-staging.azurewebsites.net',
|
|
74
|
+
production: 'https://api-admin-us-east.velaro.com',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/// Resolves an optional `env` tool argument to a target admin API base. Throws on an unrecognized
|
|
78
|
+
/// value instead of silently falling through to API_BASE (the JS default-parameter behavior of
|
|
79
|
+
/// `api(..., base)` when `base` is `undefined`) — a typo like `env: 'prod'` must not silently run
|
|
80
|
+
/// a DESTRUCTIVE call (calendly_disconnect) against whatever this server defaults to while telling
|
|
81
|
+
/// the caller it targeted a different environment. `hasOwnProperty` (not truthiness) also closes
|
|
82
|
+
/// the prototype-chain edge case (`env: 'constructor'`, `env: 'toString'`, etc.).
|
|
83
|
+
function resolveAdminBase(env) {
|
|
84
|
+
if (env === undefined || env === null) return API_BASE;
|
|
85
|
+
if (!Object.prototype.hasOwnProperty.call(ADMIN_API_HOSTS, env)) {
|
|
86
|
+
throw new Error(`Unknown env "${env}" — use "staging" or "production" (or omit it to use this server's configured default).`);
|
|
87
|
+
}
|
|
88
|
+
return ADMIN_API_HOSTS[env];
|
|
89
|
+
}
|
|
90
|
+
|
|
57
91
|
if (!MCP_KEY && !JWT) {
|
|
58
92
|
process.stderr.write('ERROR: Set VELARO_MCP_KEY=vel_live_... or VELARO_JWT=<token>\n');
|
|
59
93
|
process.exit(1);
|
|
@@ -118,8 +152,8 @@ const messagingPut = (path, body) => messagingRequest('PUT', path, body);
|
|
|
118
152
|
const messagingPatch = (path, body) => messagingRequest('PATCH', path, body);
|
|
119
153
|
const messagingDel = (path) => messagingRequest('DELETE', path);
|
|
120
154
|
|
|
121
|
-
async function api(method, path, body) {
|
|
122
|
-
const res = await fetch(`${
|
|
155
|
+
async function api(method, path, body, base = API_BASE) {
|
|
156
|
+
const res = await fetch(`${base}${path}`, {
|
|
123
157
|
method,
|
|
124
158
|
headers: { Authorization: authHeader(), 'Content-Type': 'application/json' },
|
|
125
159
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
@@ -127,6 +161,19 @@ async function api(method, path, body) {
|
|
|
127
161
|
});
|
|
128
162
|
if (!res.ok) {
|
|
129
163
|
const text = await res.text().catch(() => '');
|
|
164
|
+
if ((res.status === 401 || res.status === 403) && base !== API_BASE) {
|
|
165
|
+
// Cross-environment call with the single global credential (MCP_KEY/JWT) — this session's
|
|
166
|
+
// credential is issued for one environment, so pointing a tool at the OTHER one via an
|
|
167
|
+
// explicit `env` override will look like a real auth failure rather than "wrong environment
|
|
168
|
+
// selected." Surface that explicitly instead of a bare HTTP error, so a caller doesn't
|
|
169
|
+
// misread it as "not configured" or a generic outage.
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Velaro API ${method} ${path} -> ${res.status} against ${base}: this session's credentials ` +
|
|
172
|
+
`may not be valid for that environment (VELARO_MCP_KEY/VELARO_JWT are single, per-environment ` +
|
|
173
|
+
`credentials — re-run with credentials issued for that environment, or omit "env" to use ` +
|
|
174
|
+
`whichever environment this server is currently configured for). Raw response: ${text.slice(0, 300)}`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
130
177
|
throw new Error(`Velaro API ${method} ${path} -> ${res.status}: ${text.slice(0, 300)}`);
|
|
131
178
|
}
|
|
132
179
|
const text = await res.text();
|
|
@@ -449,14 +496,16 @@ const TOOLS = [
|
|
|
449
496
|
},
|
|
450
497
|
{
|
|
451
498
|
name: 'kb_capture_screenshot',
|
|
452
|
-
description: 'Log into admin or
|
|
499
|
+
description: 'Log into admin, messaging, or livefluence (v10) staging, navigate to a route, optionally click something (e.g. to open an export menu or a popup window), capture a screenshot, optionally polish/annotate it, upload it, and insert it into a KB article (after a matching <h2>/<h3> heading, or appended to the end). Requires E2E_USER_EMAIL/E2E_USER_PASSWORD env vars and the optional "playwright" package installed.',
|
|
453
500
|
inputSchema: {
|
|
454
501
|
type: 'object',
|
|
455
502
|
properties: {
|
|
456
|
-
app: { type: 'string', description: 'Which app to capture from', enum: ['admin', 'messaging'] },
|
|
503
|
+
app: { type: 'string', description: 'Which app to capture from', enum: ['admin', 'messaging', 'livefluence'] },
|
|
457
504
|
route: { type: 'string', description: 'Route to navigate to, e.g. "/Settings/Routing"' },
|
|
458
505
|
articleId: { type: 'number', description: 'Target article ID' },
|
|
459
506
|
heading: { type: 'string', description: 'Insert the image immediately after the <h2>/<h3> whose text contains this (case-insensitive). Falls back to appending at the end.' },
|
|
507
|
+
click: { type: 'string', description: 'Selector to click after the route loads, before capturing (CSS, or Playwright\'s "text=..."/"role=..." engines) — e.g. "text=Export"' },
|
|
508
|
+
popup: { type: 'boolean', description: 'The click target opens a NEW browser window (window.open/target=_blank) rather than something in the current page — e.g. a per-transcript export action. Capture that window instead of the original page. Requires click.' },
|
|
460
509
|
annotations: {
|
|
461
510
|
type: 'array',
|
|
462
511
|
description: 'Optional numbered/labeled circle annotations, in raw screenshot pixel coordinates.',
|
|
@@ -1195,6 +1244,47 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1195
1244
|
required: ['message'],
|
|
1196
1245
|
},
|
|
1197
1246
|
},
|
|
1247
|
+
{
|
|
1248
|
+
name: 'teams_push_settings_get',
|
|
1249
|
+
description: 'Get the Velaro Bot proactive-alert settings for this site: per-alert switches (new chat, missed chat, ticket created, ticket assigned, daily digest), the effective daily cap and the push count for today. Site is resolved server-side.',
|
|
1250
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
name: 'teams_push_settings_update',
|
|
1254
|
+
description: 'Change the Velaro Bot proactive-alert settings for this site. Only fields you pass are changed; omitted fields keep their value. Set resetDailyPushCap=true to clear the per-site cap and use the platform default.',
|
|
1255
|
+
inputSchema: {
|
|
1256
|
+
type: 'object',
|
|
1257
|
+
properties: {
|
|
1258
|
+
pushNewChat: { type: 'boolean', description: 'New chat alerts' },
|
|
1259
|
+
pushMissed: { type: 'boolean', description: 'Missed chat alerts' },
|
|
1260
|
+
pushTicketCreated: { type: 'boolean', description: 'Ticket created alerts' },
|
|
1261
|
+
pushTicketAssigned: { type: 'boolean', description: 'Ticket assigned alerts' },
|
|
1262
|
+
pushDailyDigest: { type: 'boolean', description: 'Daily digest (off by default)' },
|
|
1263
|
+
dailyPushCap: { type: 'number', description: 'Max alerts per day for this site' },
|
|
1264
|
+
resetDailyPushCap: { type: 'boolean', description: 'Clear the per-site cap (use platform default)' },
|
|
1265
|
+
},
|
|
1266
|
+
required: [],
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
name: 'teams_channel_config_get',
|
|
1271
|
+
description: 'Get this site\'s Teams Channel (customer chat) settings: whether it\'s enabled, the welcome message sent to a Teams user opening a new conversation, and the team new conversations route to (null = site default team). Site is resolved server-side.',
|
|
1272
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
name: 'teams_channel_config_update',
|
|
1276
|
+
description: 'Change this site\'s Teams Channel settings. Only fields you pass are changed. Enabling this is what makes free-text messages to the Velaro Teams bot open a real, agent-routed conversation instead of the default staff-command-only reply.',
|
|
1277
|
+
inputSchema: {
|
|
1278
|
+
type: 'object',
|
|
1279
|
+
properties: {
|
|
1280
|
+
enabled: { type: 'boolean', description: 'Turn Teams Channel (customer chat) on or off for this site' },
|
|
1281
|
+
welcomeMessage: { type: 'string', description: 'Sent to the Teams user when their message opens a new conversation. Use {{user.name}} for their display name. Max 1000 characters.' },
|
|
1282
|
+
teamId: { type: 'number', description: 'Route new conversations to this team id (must be a real, non-deleted team on this site)' },
|
|
1283
|
+
clearTeamId: { type: 'boolean', description: 'Clear the team override and fall back to the site default team' },
|
|
1284
|
+
},
|
|
1285
|
+
required: [],
|
|
1286
|
+
},
|
|
1287
|
+
},
|
|
1198
1288
|
{
|
|
1199
1289
|
name: 'teams_notify_card',
|
|
1200
1290
|
description: 'Send a rich Adaptive Card (title + fact/value fields + optional action button) to a Teams webhook — use this for structured summaries (CI results, QA report pass/fail counts, ops status) instead of teams_notify\'s plain message.',
|
|
@@ -1727,6 +1817,38 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1727
1817
|
},
|
|
1728
1818
|
},
|
|
1729
1819
|
|
|
1820
|
+
// ── Unified Outbound Activity (Klaviyo-parity Gap 6) ──────────────────────
|
|
1821
|
+
{
|
|
1822
|
+
name: 'outbound_activity_list',
|
|
1823
|
+
description: 'Unified outbound email feed across ALL THREE send systems at once: transactional/system mail (password resets, alerts), sequence/drip campaign steps, and one-off broadcasts. Newest first. Use this instead of checking the broadcast, sequence and transactional views separately. Site scoping is enforced server-side.',
|
|
1824
|
+
inputSchema: {
|
|
1825
|
+
type: 'object',
|
|
1826
|
+
properties: {
|
|
1827
|
+
take: { type: 'number', description: 'Page size, 1-100 (default 25)' },
|
|
1828
|
+
skip: { type: 'number', description: 'Rows to skip (default 0)' },
|
|
1829
|
+
source: { type: 'string', enum: ['Transactional', 'Campaign', 'Broadcast'], description: 'Limit to one send system. Omit for all three.' },
|
|
1830
|
+
status: { type: 'string', enum: ['Sent', 'Pending', 'Failed', 'Unsubscribed', 'Skipped'], description: 'Normalized status shared across all three systems' },
|
|
1831
|
+
search: { type: 'string', description: 'Match recipient address or subject' },
|
|
1832
|
+
},
|
|
1833
|
+
required: [],
|
|
1834
|
+
},
|
|
1835
|
+
},
|
|
1836
|
+
{
|
|
1837
|
+
name: 'outbound_activity_for_contact',
|
|
1838
|
+
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.',
|
|
1839
|
+
inputSchema: {
|
|
1840
|
+
type: 'object',
|
|
1841
|
+
properties: {
|
|
1842
|
+
email: { type: 'string', description: 'Exact recipient email address (required)' },
|
|
1843
|
+
take: { type: 'number', description: 'Page size, 1-100 (default 25)' },
|
|
1844
|
+
skip: { type: 'number', description: 'Rows to skip (default 0)' },
|
|
1845
|
+
source: { type: 'string', enum: ['Transactional', 'Campaign', 'Broadcast'], description: 'Limit to one send system. Omit for all three.' },
|
|
1846
|
+
status: { type: 'string', enum: ['Sent', 'Pending', 'Failed', 'Unsubscribed', 'Skipped'], description: 'Normalized status shared across all three systems' },
|
|
1847
|
+
},
|
|
1848
|
+
required: ['email'],
|
|
1849
|
+
},
|
|
1850
|
+
},
|
|
1851
|
+
|
|
1730
1852
|
// ── Conversion tracking tools ─────────────────────────────────────────────
|
|
1731
1853
|
{
|
|
1732
1854
|
name: 'conversion_list_goals',
|
|
@@ -2112,6 +2234,15 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2112
2234
|
required: ['siteId'],
|
|
2113
2235
|
},
|
|
2114
2236
|
},
|
|
2237
|
+
{
|
|
2238
|
+
name: 'support_site_deployments',
|
|
2239
|
+
description: 'List active widget deployment embed keys and assigned survey IDs for one site. Staff-only and read-only. Use deploymentId (the string embed key) in the widget test harness, not the numeric database id.',
|
|
2240
|
+
inputSchema: {
|
|
2241
|
+
type: 'object',
|
|
2242
|
+
properties: { siteId: { type: 'number', description: 'Target site ID' } },
|
|
2243
|
+
required: ['siteId'],
|
|
2244
|
+
},
|
|
2245
|
+
},
|
|
2115
2246
|
{
|
|
2116
2247
|
name: 'support_site_agents',
|
|
2117
2248
|
description: 'List all agents/admins on a customer site with their roles, active status, and last login date. Velaro staff only.',
|
|
@@ -2231,11 +2362,12 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2231
2362
|
// ── Calendly integration management (superadmin) ──────────────────────────
|
|
2232
2363
|
{
|
|
2233
2364
|
name: 'calendly_get_status',
|
|
2234
|
-
description: 'Check whether Calendly is configured for a site and show the current settings. Velaro admin only.',
|
|
2365
|
+
description: 'Check whether Calendly is configured for a site and show the current settings. Velaro admin only. Queries whichever admin API environment this server is currently configured for (VELARO_ADMIN_API) unless "env" is given explicitly.',
|
|
2235
2366
|
inputSchema: {
|
|
2236
2367
|
type: 'object',
|
|
2237
2368
|
properties: {
|
|
2238
2369
|
siteId: { type: 'number', description: 'Site ID to check' },
|
|
2370
|
+
env: { type: 'string', enum: ['staging', 'production'], description: 'Explicitly target one environment instead of this server\'s configured default (VELARO_ADMIN_API). Use this when a site is known to live in one specific environment (e.g. a staging-only test site) — omitting it can silently report "not configured" for a site that is only set up in the OTHER environment from the one this server defaults to. Requires credentials valid for that environment.' },
|
|
2239
2371
|
},
|
|
2240
2372
|
required: ['siteId'],
|
|
2241
2373
|
},
|
|
@@ -2252,6 +2384,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2252
2384
|
defaultEventTypeName: { type: 'string', description: 'Human-readable name for the default event type. Optional.' },
|
|
2253
2385
|
displayName: { type: 'string', description: 'Label shown in admin UI, e.g. "Acme Demo Booking". Optional.' },
|
|
2254
2386
|
useSingleUseLinks:{ type: 'boolean', description: 'true = generate single-use links per visitor (requires scheduling_links:write). false = reusable event-type URL. Default: true.' },
|
|
2387
|
+
env: { type: 'string', enum: ['staging', 'production'], description: 'Explicitly target one environment instead of this server\'s configured default (VELARO_ADMIN_API). Requires credentials valid for that environment.' },
|
|
2255
2388
|
},
|
|
2256
2389
|
required: ['siteId', 'accessToken'],
|
|
2257
2390
|
},
|
|
@@ -2263,6 +2396,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2263
2396
|
type: 'object',
|
|
2264
2397
|
properties: {
|
|
2265
2398
|
siteId: { type: 'number', description: 'Site ID to disconnect' },
|
|
2399
|
+
env: { type: 'string', enum: ['staging', 'production'], description: 'Explicitly target one environment instead of this server\'s configured default (VELARO_ADMIN_API). Requires credentials valid for that environment.' },
|
|
2266
2400
|
},
|
|
2267
2401
|
required: ['siteId'],
|
|
2268
2402
|
},
|
|
@@ -3316,6 +3450,18 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3316
3450
|
required: ['siteId', 'planKey'],
|
|
3317
3451
|
},
|
|
3318
3452
|
},
|
|
3453
|
+
{ // wires EntitlementsController.SetEntitlementLock (PUT Entitlements/admin/lock/{siteId})
|
|
3454
|
+
name: 'entitlement_set_lock',
|
|
3455
|
+
description: 'Lock a site\'s entitlement resolution to a point-in-time PlanEntitlement version (freezes what plan changes the site sees), or clear an existing lock so it resolves against the current plan version again. Pass lockedAt=null to unlock. Refuses to lock to a timestamp with no defined PlanEntitlement row for the site\'s current plan. Superadmin only.',
|
|
3456
|
+
inputSchema: {
|
|
3457
|
+
type: 'object',
|
|
3458
|
+
properties: {
|
|
3459
|
+
siteId: { type: 'number', description: 'Site ID to lock or unlock.' },
|
|
3460
|
+
lockedAt: { type: 'string', description: 'ISO 8601 timestamp to lock entitlement resolution to. Pass null (or omit) to clear an existing lock.' },
|
|
3461
|
+
},
|
|
3462
|
+
required: ['siteId'],
|
|
3463
|
+
},
|
|
3464
|
+
},
|
|
3319
3465
|
{
|
|
3320
3466
|
name: 'entitlement_seed_plans',
|
|
3321
3467
|
description: 'Seed PlanEntitlement rows from live PackageVersions in the messaging DB. Maps each plan×feature to its current value. Idempotent — inserts new rows, versions changed rows. Run after entitlement_seed and after any PackageVersion change. Superadmin only.',
|
|
@@ -4318,6 +4464,8 @@ async function handleTool(name, args) {
|
|
|
4318
4464
|
articleId: args.articleId,
|
|
4319
4465
|
heading: args.heading,
|
|
4320
4466
|
annotations: args.annotations ?? [],
|
|
4467
|
+
click: args.click,
|
|
4468
|
+
popup: args.popup ?? false,
|
|
4321
4469
|
auth: { token, apiBase: API_BASE },
|
|
4322
4470
|
});
|
|
4323
4471
|
return result.insertedAfterHeading
|
|
@@ -4899,6 +5047,26 @@ async function handleTool(name, args) {
|
|
|
4899
5047
|
return `Sent to Teams webhook "${webhook.name}".`;
|
|
4900
5048
|
}
|
|
4901
5049
|
|
|
5050
|
+
case 'teams_push_settings_get':
|
|
5051
|
+
return await api('GET', '/TeamsBot/PushSettings');
|
|
5052
|
+
|
|
5053
|
+
case 'teams_push_settings_update': {
|
|
5054
|
+
const body = {};
|
|
5055
|
+
for (const k of ['pushNewChat','pushMissed','pushTicketCreated','pushTicketAssigned','pushDailyDigest','dailyPushCap','resetDailyPushCap'])
|
|
5056
|
+
if (args[k] !== undefined) body[k] = args[k];
|
|
5057
|
+
return await api('PUT', '/TeamsBot/PushSettings', body);
|
|
5058
|
+
}
|
|
5059
|
+
|
|
5060
|
+
case 'teams_channel_config_get':
|
|
5061
|
+
return await api('GET', '/TeamsBot/ChannelConfig');
|
|
5062
|
+
|
|
5063
|
+
case 'teams_channel_config_update': {
|
|
5064
|
+
const body = {};
|
|
5065
|
+
for (const k of ['enabled','welcomeMessage','teamId','clearTeamId'])
|
|
5066
|
+
if (args[k] !== undefined) body[k] = args[k];
|
|
5067
|
+
return await api('PUT', '/TeamsBot/ChannelConfig', body);
|
|
5068
|
+
}
|
|
5069
|
+
|
|
4902
5070
|
case 'teams_notify_card': {
|
|
4903
5071
|
const webhooks = await api('GET', '/TeamsNotification');
|
|
4904
5072
|
if (!Array.isArray(webhooks) || !webhooks.length)
|
|
@@ -6004,6 +6172,57 @@ async function handleTool(name, args) {
|
|
|
6004
6172
|
return lines.join('\n');
|
|
6005
6173
|
}
|
|
6006
6174
|
|
|
6175
|
+
// ── Unified Outbound Activity ──────────────────────────────────────────
|
|
6176
|
+
case 'outbound_activity_list':
|
|
6177
|
+
case 'outbound_activity_for_contact': {
|
|
6178
|
+
const forContact = name === 'outbound_activity_for_contact';
|
|
6179
|
+
const email = String(args.email ?? '').trim();
|
|
6180
|
+
// Required scoping identifier — never fall through to a site-wide listing.
|
|
6181
|
+
if (forContact && !email) return 'Error: email is required for outbound_activity_for_contact';
|
|
6182
|
+
|
|
6183
|
+
const params = new URLSearchParams();
|
|
6184
|
+
params.set('take', String(args.take ?? 25));
|
|
6185
|
+
params.set('skip', String(args.skip ?? 0));
|
|
6186
|
+
if (args.source) params.set('source', args.source);
|
|
6187
|
+
if (args.status) params.set('status', args.status);
|
|
6188
|
+
if (!forContact && args.search) params.set('search', args.search);
|
|
6189
|
+
|
|
6190
|
+
const path = forContact
|
|
6191
|
+
? `/Marketing/Contacts/${encodeURIComponent(email)}/OutboundActivity?${params}`
|
|
6192
|
+
: `/Marketing/OutboundActivity?${params}`;
|
|
6193
|
+
|
|
6194
|
+
const r = await api('GET', path);
|
|
6195
|
+
const items = r?.items ?? [];
|
|
6196
|
+
const srcLabel = { Transactional: 'Transactional', Campaign: 'Sequence', Broadcast: 'Broadcast' };
|
|
6197
|
+
|
|
6198
|
+
const lines = [forContact ? `=== Outbound Activity: ${email} ===` : '=== Outbound Activity ==='];
|
|
6199
|
+
|
|
6200
|
+
// A partial feed is never reported as if it were complete.
|
|
6201
|
+
if (r?.degradedSources?.length) {
|
|
6202
|
+
lines.push(` ! INCOMPLETE - could not load: ${r.degradedSources.map(s => srcLabel[s] ?? s).join(', ')}. Rows from those systems are MISSING below.`);
|
|
6203
|
+
}
|
|
6204
|
+
if (!items.length) {
|
|
6205
|
+
lines.push(' (no outbound activity matched)');
|
|
6206
|
+
return lines.join('\n');
|
|
6207
|
+
}
|
|
6208
|
+
|
|
6209
|
+
for (const i of items) {
|
|
6210
|
+
const when = i.sentAt ? new Date(i.sentAt).toISOString() : 'not sent';
|
|
6211
|
+
lines.push(` [${i.status}] ${srcLabel[i.source] ?? i.source} ${when} -> ${i.toEmail ?? '(none)'}`);
|
|
6212
|
+
lines.push(` ${i.subject ?? ''}`);
|
|
6213
|
+
const meta = [];
|
|
6214
|
+
if (i.campaignOrBroadcastName) meta.push(`campaign: ${i.campaignOrBroadcastName}`);
|
|
6215
|
+
if (i.tag) meta.push(`tag: ${i.tag}`);
|
|
6216
|
+
if (i.channel && i.channel !== 'email') meta.push(`channel: ${i.channel}`);
|
|
6217
|
+
// Transactional sends have no open/click tracking at all — report nothing rather than a
|
|
6218
|
+
// zero that would read as a measured zero.
|
|
6219
|
+
if (i.source !== 'Transactional') meta.push(`opens ${i.openCount}, clicks ${i.clickCount}`);
|
|
6220
|
+
if (meta.length) lines.push(` ${meta.join(' | ')}`);
|
|
6221
|
+
}
|
|
6222
|
+
lines.push(` ${items.length} shown of ${r?.totalCountIsApproximate ? 'about ' : ''}${r?.totalCount ?? items.length}.`);
|
|
6223
|
+
return lines.join('\n');
|
|
6224
|
+
}
|
|
6225
|
+
|
|
6007
6226
|
// ── Audit Trail ────────────────────────────────────────────────────────
|
|
6008
6227
|
case 'audit_trail_list': {
|
|
6009
6228
|
const params = new URLSearchParams();
|
|
@@ -6376,6 +6595,14 @@ async function handleTool(name, args) {
|
|
|
6376
6595
|
].join('\n');
|
|
6377
6596
|
}
|
|
6378
6597
|
|
|
6598
|
+
case 'support_site_deployments': {
|
|
6599
|
+
const data = await api('GET', `/SupportTools/sites/${args.siteId}/deployments`);
|
|
6600
|
+
if (!data.deployments?.length) return `No active widget deployments for site ${args.siteId}.`;
|
|
6601
|
+
return data.deployments.map(d =>
|
|
6602
|
+
`[id=${d.id}] ${d.displayName || '(unnamed)'} | embed key: ${d.deploymentId} | team: ${d.teamId} | surveys: pre-chat ${d.preChatSurveyId ?? '—'}, post-chat ${d.postChatSurveyId ?? '—'}, unavailable ${d.unavailableSurveyId ?? '—'}`
|
|
6603
|
+
).join('\n');
|
|
6604
|
+
}
|
|
6605
|
+
|
|
6379
6606
|
case 'support_site_agents': {
|
|
6380
6607
|
const agents = await api('GET', `/SupportTools/sites/${args.siteId}/agents`);
|
|
6381
6608
|
if (!Array.isArray(agents) || !agents.length) return `No agents found for site ${args.siteId}.`;
|
|
@@ -6468,10 +6695,26 @@ async function handleTool(name, args) {
|
|
|
6468
6695
|
}
|
|
6469
6696
|
|
|
6470
6697
|
case 'calendly_get_status': {
|
|
6471
|
-
|
|
6472
|
-
|
|
6698
|
+
// `env`, when given, targets one specific admin API environment instead of whatever
|
|
6699
|
+
// VELARO_ADMIN_API this server process happens to be configured with — see ADMIN_API_HOSTS
|
|
6700
|
+
// and resolveAdminBase() above. No default override here: an unspecified `env` means "use
|
|
6701
|
+
// this server's configured environment," same as every other tool in this file.
|
|
6702
|
+
const base = resolveAdminBase(args.env);
|
|
6703
|
+
const data = await api('GET', `/Calendly/superadmin/status?siteId=${args.siteId}`, undefined, base);
|
|
6704
|
+
const envSuffix = ` (${args.env ?? 'this server\'s configured env'}: ${base})`;
|
|
6705
|
+
if (!data.configured) {
|
|
6706
|
+
// exists === true means a row IS present but Enabled is false — a real, different state
|
|
6707
|
+
// from "never configured," which used to render identically (see CalendlyController.cs
|
|
6708
|
+
// SuperAdminStatus) and is one plausible (unconfirmed) explanation for the 2026-09-08
|
|
6709
|
+
// Donaldson bot Calendly status contradiction. exists === undefined means this admin API
|
|
6710
|
+
// build predates the field (staging/production deploy independently) — don't assert a
|
|
6711
|
+
// specific DB state the response didn't actually report.
|
|
6712
|
+
if (data.exists === true) return `Site ${args.siteId}${envSuffix}: Calendly configured but DISABLED (a config row exists with Enabled=false).`;
|
|
6713
|
+
if (data.exists === undefined) return `Site ${args.siteId}${envSuffix}: Calendly not enabled. (This API build does not report "exists" yet, so a disabled row can't be distinguished from no row at all — re-check once this endpoint is deployed there.)`;
|
|
6714
|
+
return `Site ${args.siteId}${envSuffix}: Calendly NOT configured (no config row found in this environment).`;
|
|
6715
|
+
}
|
|
6473
6716
|
return [
|
|
6474
|
-
`Site ${args.siteId}: Calendly configured ✓`,
|
|
6717
|
+
`Site ${args.siteId}${envSuffix}: Calendly configured ✓`,
|
|
6475
6718
|
` Account: ${data.userName}`,
|
|
6476
6719
|
` Default type: ${data.defaultEventTypeName || '(none)'}`,
|
|
6477
6720
|
` Single-use: ${data.useSingleUseLinks ? 'yes' : 'no'}`,
|
|
@@ -6480,20 +6723,22 @@ async function handleTool(name, args) {
|
|
|
6480
6723
|
}
|
|
6481
6724
|
|
|
6482
6725
|
case 'calendly_configure': {
|
|
6726
|
+
const base = resolveAdminBase(args.env);
|
|
6483
6727
|
const res = await api('POST', `/Calendly/superadmin/config?siteId=${args.siteId}`, {
|
|
6484
6728
|
accessToken: args.accessToken,
|
|
6485
6729
|
displayName: args.displayName ?? '',
|
|
6486
6730
|
defaultEventTypeUri: args.defaultEventTypeUri ?? '',
|
|
6487
6731
|
defaultEventTypeName: args.defaultEventTypeName ?? '',
|
|
6488
6732
|
useSingleUseLinks: args.useSingleUseLinks ?? true,
|
|
6489
|
-
});
|
|
6733
|
+
}, base);
|
|
6490
6734
|
if (!res?.success) throw new Error(res?.error || 'Configure failed');
|
|
6491
|
-
return `✓ Calendly configured for site ${args.siteId}. Account: ${res.userName}`;
|
|
6735
|
+
return `✓ Calendly configured for site ${args.siteId} (${args.env ?? 'this server\'s configured env'}: ${base}). Account: ${res.userName}`;
|
|
6492
6736
|
}
|
|
6493
6737
|
|
|
6494
6738
|
case 'calendly_disconnect': {
|
|
6495
|
-
|
|
6496
|
-
|
|
6739
|
+
const base = resolveAdminBase(args.env);
|
|
6740
|
+
await api('DELETE', `/Calendly/superadmin/config?siteId=${args.siteId}`, undefined, base);
|
|
6741
|
+
return `✓ Calendly disconnected from site ${args.siteId} (${args.env ?? 'this server\'s configured env'}: ${base}).`;
|
|
6497
6742
|
}
|
|
6498
6743
|
|
|
6499
6744
|
// ── Acuity Scheduling ─────────────────────────────────────────────────────
|
|
@@ -7641,6 +7886,15 @@ async function handleTool(name, args) {
|
|
|
7641
7886
|
return `✅ Site ${args.siteId} comped on plan ${args.planKey}${expiry}. Use entitlement_set_override to customize features.`;
|
|
7642
7887
|
}
|
|
7643
7888
|
|
|
7889
|
+
case 'entitlement_set_lock': {
|
|
7890
|
+
if (!args.siteId) throw new Error('siteId is required');
|
|
7891
|
+
const lockedAt = args.lockedAt ?? null;
|
|
7892
|
+
const result = await api('PUT', `/Entitlements/admin/lock/${args.siteId}`, { lockedAt });
|
|
7893
|
+
return lockedAt
|
|
7894
|
+
? `✅ Site ${args.siteId} entitlements locked to ${result.entitlementLockedAt} (plan ${result.planKey}).`
|
|
7895
|
+
: `✅ Site ${args.siteId} entitlement lock cleared. Resolves against the current plan version again.`;
|
|
7896
|
+
}
|
|
7897
|
+
|
|
7644
7898
|
case 'entitlement_seed_plans': {
|
|
7645
7899
|
const result = await api('POST', '/Entitlements/admin/seed-plans');
|
|
7646
7900
|
if (result.error) return `❌ ${result.error}`;
|