@velaro/mcp-server 2.0.0 → 2.0.2
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/fetch-retry.js +23 -0
- package/messaging-base.js +1 -1
- package/package.json +3 -2
- package/parity/activity.customer.js +1 -1
- package/report-explain.json +232 -0
- package/server.js +182 -156
- package/tool-annotations.js +77 -75
- package/log-search.js +0 -252
package/server.js
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* VELARO_MCP_MODE=http PORT=3000 node server.js
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
import {
|
|
43
|
+
import { fetchRetry429 } from './fetch-retry.js';
|
|
44
44
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
45
45
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
46
46
|
import {
|
|
@@ -54,51 +54,12 @@ import { loadParity, runParity } from './parity/loader.js';
|
|
|
54
54
|
import { resolveMessagingApi } from './messaging-base.js';
|
|
55
55
|
import { buildResolvePath, buildTimelinePath, buildOverrideBody, buildValidateBody, buildBannerPath, buildOverrideListPath, tolerant, VALIDATE_PATH, OVERRIDE_PATH } from './workflow-schedule.js';
|
|
56
56
|
|
|
57
|
-
//
|
|
58
|
-
// clarity during the 2026-09-10 Donaldson bot Calendly status investigation
|
|
59
|
-
// (docs/qa/chatbot-hallucinated-booking-donaldson-bot.md) — but this is NOT a functional fix and
|
|
60
|
-
// does NOT explain that incident: live-verified same day that 'https://help.velaro.com' and
|
|
61
|
-
// 'https://api-admin-us-east.velaro.com' are the SAME deployment (identical /api/health build SHA,
|
|
62
|
-
// and /Calendly/superadmin/status returns 401 — auth-gated, not 404 — on both hosts). Whatever
|
|
63
|
-
// caused that investigation's status/reality contradiction, it was NOT this default resolving to a
|
|
64
|
-
// broken or different host; that theory is disproven. Left as a naming clarity improvement only.
|
|
57
|
+
// Production admin API by default; set VELARO_ADMIN_API to target staging.
|
|
65
58
|
const API_BASE = process.env.VELARO_ADMIN_API || 'https://api-admin-us-east.velaro.com';
|
|
66
59
|
const MESSAGING_API = resolveMessagingApi(process.env.VELARO_MESSAGING_API, API_BASE);
|
|
67
60
|
const MCP_KEY = process.env.VELARO_MCP_KEY;
|
|
68
61
|
const JWT = process.env.VELARO_JWT;
|
|
69
62
|
|
|
70
|
-
// mcp/staff-server.js defaults to staging instead -- the two servers' defaults intentionally
|
|
71
|
-
// differ, but a zero-result lookup gives no hint which environment was actually searched
|
|
72
|
-
// either way. See ENV_LABEL in staff-server.js for the incident this class of confusion caused
|
|
73
|
-
// (a real production customer read as "doesn't exist" from a staging-scoped search).
|
|
74
|
-
const ENV_LABEL = API_BASE.includes('staging') ? 'staging' : 'production';
|
|
75
|
-
|
|
76
|
-
// Known admin API hosts, for tools that let a caller explicitly target one environment
|
|
77
|
-
// regardless of what this server process's own API_BASE is configured to (see the `env` param
|
|
78
|
-
// on the calendly_* superadmin tools below). Deliberately NOT used as a default anywhere —
|
|
79
|
-
// defaulting env-selectable tools to one hardcoded environment while every other tool in this
|
|
80
|
-
// file follows the single global API_BASE would recreate the exact bug being fixed here: an
|
|
81
|
-
// operator configured for one environment would silently get a DIFFERENT environment's answer
|
|
82
|
-
// from just these tools with no indication anything was different.
|
|
83
|
-
const ADMIN_API_HOSTS = {
|
|
84
|
-
staging: 'https://velaro-admin-staging.azurewebsites.net',
|
|
85
|
-
production: 'https://api-admin-us-east.velaro.com',
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/// Resolves an optional `env` tool argument to a target admin API base. Throws on an unrecognized
|
|
89
|
-
/// value instead of silently falling through to API_BASE (the JS default-parameter behavior of
|
|
90
|
-
/// `api(..., base)` when `base` is `undefined`) — a typo like `env: 'prod'` must not silently run
|
|
91
|
-
/// a DESTRUCTIVE call (calendly_disconnect) against whatever this server defaults to while telling
|
|
92
|
-
/// the caller it targeted a different environment. `hasOwnProperty` (not truthiness) also closes
|
|
93
|
-
/// the prototype-chain edge case (`env: 'constructor'`, `env: 'toString'`, etc.).
|
|
94
|
-
function resolveAdminBase(env) {
|
|
95
|
-
if (env === undefined || env === null) return API_BASE;
|
|
96
|
-
if (!Object.prototype.hasOwnProperty.call(ADMIN_API_HOSTS, env)) {
|
|
97
|
-
throw new Error(`Unknown env "${env}" — use "staging" or "production" (or omit it to use this server's configured default).`);
|
|
98
|
-
}
|
|
99
|
-
return ADMIN_API_HOSTS[env];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
63
|
if (!MCP_KEY && !JWT) {
|
|
103
64
|
process.stderr.write('ERROR: Set VELARO_MCP_KEY=vel_live_... or VELARO_JWT=<token>\n');
|
|
104
65
|
process.exit(1);
|
|
@@ -115,15 +76,6 @@ function formatHealthRow(item) {
|
|
|
115
76
|
return ` ${(item.slug ?? '?').padEnd(14)} ${(item.status ?? '?').padEnd(14)} ${ms.padStart(7)} ${ts} ${item.detail ?? ''}`.trimEnd();
|
|
116
77
|
}
|
|
117
78
|
|
|
118
|
-
function parseDuration(s) {
|
|
119
|
-
const m = /^(\d+)(m|h|d)$/i.exec(String(s ?? '2h').trim());
|
|
120
|
-
if (!m) return 2 * 3600 * 1000;
|
|
121
|
-
const n = parseInt(m[1], 10);
|
|
122
|
-
if (m[2] === 'm') return n * 60 * 1000;
|
|
123
|
-
if (m[2] === 'h') return n * 3600 * 1000;
|
|
124
|
-
return n * 24 * 3600 * 1000;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
79
|
async function messagingApi(path, opts = {}) { // opts.timeoutMs: log searches pass 40000 (server scan budget 12 s + 6 s AI search)
|
|
128
80
|
if (!MESSAGING_API) throw new Error(
|
|
129
81
|
'VELARO_MESSAGING_API is not configured. Add "VELARO_MESSAGING_API": "https://velaro-messaging-api-staging.azurewebsites.net" to your MCP env config.'
|
|
@@ -133,7 +85,6 @@ async function messagingApi(path, opts = {}) { // opts.timeoutMs: log searches
|
|
|
133
85
|
method: opts.method ?? 'GET',
|
|
134
86
|
headers: {
|
|
135
87
|
Authorization: authHeader(),
|
|
136
|
-
'X-Internal-SiteId': '1032',
|
|
137
88
|
...(opts.body ? { 'Content-Type': 'application/json' } : {}),
|
|
138
89
|
},
|
|
139
90
|
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
@@ -142,22 +93,11 @@ async function messagingApi(path, opts = {}) { // opts.timeoutMs: log searches
|
|
|
142
93
|
const res = (opts.method ?? 'GET') === 'GET' ? await fetchRetry429(`${MESSAGING_API}${path}`, init) : await fetch(`${MESSAGING_API}${path}`, init());
|
|
143
94
|
if (!res.ok) {
|
|
144
95
|
const text = await res.text().catch(() => '');
|
|
145
|
-
throw new Error(`
|
|
96
|
+
throw new Error(`Messaging API ${path} -> ${res.status}: ${text.slice(0, 300)}`);
|
|
146
97
|
}
|
|
147
98
|
return res.json();
|
|
148
99
|
}
|
|
149
100
|
|
|
150
|
-
// FIXED 2026-09-24: velaro-messaging's /superadmin/logs/search used to return a bare JSON array
|
|
151
|
-
// for some queries and {entries, nextCursor} for others, depending on which internal code path
|
|
152
|
-
// served the request (see that repo's LogsController.cs ENVELOPE SHAPE comment). It now always
|
|
153
|
-
// returns {entries, nextCursor} -- this helper also accepts a bare array so callers degrade
|
|
154
|
-
// gracefully rather than throwing if ever pointed at an older deploy.
|
|
155
|
-
function extractLogEntries(response) {
|
|
156
|
-
if (Array.isArray(response)) return response;
|
|
157
|
-
if (response && Array.isArray(response.entries)) return response.entries;
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
101
|
/** Body for POST/PUT ApplePayPresetOrders: only passed fields; the server computes the total. */
|
|
162
102
|
function applePayPresetBody(args) {
|
|
163
103
|
const body = {};
|
|
@@ -334,51 +274,9 @@ async function api(method, path, body, base = API_BASE) {
|
|
|
334
274
|
return text ? JSON.parse(text) : null;
|
|
335
275
|
}
|
|
336
276
|
|
|
337
|
-
// ── Livefluence migration helpers ─────────────────────────────────────────────
|
|
338
|
-
|
|
339
277
|
import { fileURLToPath as _ftu } from 'node:url';
|
|
340
278
|
import { join as _join, dirname as _dirname } from 'node:path';
|
|
341
|
-
import { readFileSync as _readFileSync
|
|
342
|
-
|
|
343
|
-
const _migrationsDir = _join(_dirname(_ftu(import.meta.url)), '..', 'cli', 'lib', 'migrations');
|
|
344
|
-
|
|
345
|
-
function loadMigrationProfile(slug) {
|
|
346
|
-
if (!slug) throw new Error('Provide customer slug (azenta, amscro, bluefire, donaldson) or a custom profile object.');
|
|
347
|
-
const file = _join(_migrationsDir, `${slug}.json`);
|
|
348
|
-
if (!_existsSync(file)) {
|
|
349
|
-
throw new Error(`Migration profile "${slug}" not found. Available: azenta, amscro, bluefire, donaldson`);
|
|
350
|
-
}
|
|
351
|
-
return JSON.parse(_readFileSync(file, 'utf8'));
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function mcpResolveTokens(tokenMap, items, idField, nameField) {
|
|
355
|
-
const out = {};
|
|
356
|
-
for (const [token, name] of Object.entries(tokenMap)) {
|
|
357
|
-
const match = (items ?? []).find(i => i[nameField]?.toLowerCase() === name.toLowerCase());
|
|
358
|
-
out[`@${token}`] = match ? String(match[idField]) : null;
|
|
359
|
-
}
|
|
360
|
-
return out;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function mcpResolveValue(value, aiMap, teamMap) {
|
|
364
|
-
if (!value) return value;
|
|
365
|
-
const out = {};
|
|
366
|
-
for (const [k, v] of Object.entries(value)) {
|
|
367
|
-
if (typeof v === 'string' && v.startsWith('@')) {
|
|
368
|
-
out[k] = aiMap[v] ?? teamMap[v] ?? v;
|
|
369
|
-
} else {
|
|
370
|
-
out[k] = v;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return out;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function mcpFindUnresolved(value) {
|
|
377
|
-
if (!value) return [];
|
|
378
|
-
return Object.entries(value)
|
|
379
|
-
.filter(([, v]) => v === null || (typeof v === 'string' && v.startsWith('@')))
|
|
380
|
-
.map(([k]) => k);
|
|
381
|
-
}
|
|
279
|
+
import { readFileSync as _readFileSync } from 'node:fs';
|
|
382
280
|
|
|
383
281
|
// The hosted gateway requires an initialized session (Mcp-Session-Id + MCP-Protocol-Version) on
|
|
384
282
|
// every call after "initialize". Cache one per process and re-initialize when it expires (404).
|
|
@@ -1067,6 +965,8 @@ Key node shapes:
|
|
|
1067
965
|
propName:"myAnswerVar" } // variable that stores the result as JSON array string
|
|
1068
966
|
• condition: { type:"action", actionType:"condition", conditionOptions:[...] }
|
|
1069
967
|
• transferToAI: { type:"action", actionType:"transferToAI", aiConfigId:N }
|
|
968
|
+
• selectPreChat: { type:"action", actionType:"selectPreChat", surveyId:N, surveyCompact:true|false|null, child:N }
|
|
969
|
+
surveyCompact (optional tri-state): null/omitted = follow the survey's own compactMode, true = compact, false = regular.
|
|
1070
970
|
• sendSurvey: { type:"action", actionType:"sendSurvey", sendSurveyId:N, surveyDeliveryMode:"conversational"|"form", closeAfterSurvey:true|false, child:N }
|
|
1071
971
|
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.
|
|
1072
972
|
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.
|
|
@@ -1132,13 +1032,14 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1132
1032
|
},
|
|
1133
1033
|
{
|
|
1134
1034
|
name: 'survey_upsert',
|
|
1135
|
-
description: 'Create (omit id or set id=0) or update (set id) a survey. body must be the full SurveyViewModel JSON shape (id, name, questions[], each question with type/text/options/required/variable/etc.). Use survey_get on an existing survey to fetch this full shape as a template rather than guessing the question schema from scratch. Each question also accepts isThemeAnchor (boolean, default false: this answer sets the Chat Intelligence topic) and themeMapping (string: JSON object mapping answer text to a site topic, e.g. "{\\"Pay my bill\\":\\"Billing Questions\\"}"). Both only take effect when the site has the multi-topic setting on.',
|
|
1035
|
+
description: 'Create (omit id or set id=0) or update (set id) a survey. body must be the full SurveyViewModel JSON shape (id, name, questions[], each question with type/text/options/required/variable/etc.). Use survey_get on an existing survey to fetch this full shape as a template rather than guessing the question schema from scratch. Each question also accepts isThemeAnchor (boolean, default false: this answer sets the Chat Intelligence topic) and themeMapping (string: JSON object mapping answer text to a site topic, e.g. "{\\"Pay my bill\\":\\"Billing Questions\\"}"). Both only take effect when the site has the multi-topic setting on. body.compactMode is tri-state: null/omitted = follow default (regular size), true = compact, false = regular.',
|
|
1136
1036
|
inputSchema: {
|
|
1137
1037
|
type: 'object',
|
|
1138
1038
|
properties: { body: {
|
|
1139
1039
|
type: 'object',
|
|
1140
1040
|
description: 'Full SurveyViewModel JSON',
|
|
1141
1041
|
properties: {
|
|
1042
|
+
compactMode: { type: ['boolean', 'null'], description: 'Tri-state survey size: null/omitted = follow default (regular size), true = compact, false = regular' },
|
|
1142
1043
|
questions: {
|
|
1143
1044
|
type: 'array',
|
|
1144
1045
|
items: {
|
|
@@ -1582,6 +1483,28 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1582
1483
|
},
|
|
1583
1484
|
},
|
|
1584
1485
|
|
|
1486
|
+
{
|
|
1487
|
+
name: 'agent_force_sign_out',
|
|
1488
|
+
description: 'Security control: force one agent to sign in again. Revokes every active session of that agent now (their existing tokens stop working) and sets them Offline. Administrator only; the agent must belong to your site; audited (who/when/target). You cannot force sign out your own account.',
|
|
1489
|
+
inputSchema: {
|
|
1490
|
+
type: 'object',
|
|
1491
|
+
properties: { userId: { type: 'number', description: 'Agent user id from agent_list' } },
|
|
1492
|
+
required: ['userId'],
|
|
1493
|
+
},
|
|
1494
|
+
},
|
|
1495
|
+
{
|
|
1496
|
+
name: 'agent_set_status',
|
|
1497
|
+
description: 'Set an agent availability status (Available, Busy, Away, Offline). Use Offline or Away to make an agent Unavailable for routing. Site manager only; the agent must belong to your site; audited with the acting admin.',
|
|
1498
|
+
inputSchema: {
|
|
1499
|
+
type: 'object',
|
|
1500
|
+
properties: {
|
|
1501
|
+
userId: { type: 'number', description: 'Agent user id from agent_list' },
|
|
1502
|
+
status: { type: 'string', enum: ['Available', 'Busy', 'Away', 'Offline'] },
|
|
1503
|
+
},
|
|
1504
|
+
required: ['userId', 'status'],
|
|
1505
|
+
},
|
|
1506
|
+
},
|
|
1507
|
+
|
|
1585
1508
|
// ── Dynamics per-team overrides (effective = team override ?? connection default) ──
|
|
1586
1509
|
{
|
|
1587
1510
|
name: 'dynamics_team_overrides_get',
|
|
@@ -1714,8 +1637,6 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1714
1637
|
},
|
|
1715
1638
|
buttonLabel: { type: 'string', description: 'Optional action button label' },
|
|
1716
1639
|
buttonUrl: { type: 'string', description: 'Optional action button URL (required if buttonLabel is set)' },
|
|
1717
|
-
mentions: { type: 'array', description: 'People to @mention (real Teams notification). Velaro staff only. Uses the ops Teams sender instead of a webhook.', items: { type: 'object', properties: { user: { type: 'string', description: 'Entra object id or UPN, e.g. andrea@velaro.com' }, name: { type: 'string', description: 'Display name shown in the @mention' } }, required: ['user'] } },
|
|
1718
|
-
destination: { type: 'string', description: 'Named channel in the DevOps team instead of General: prs, workflows, deploys, alerts, qa, general. Velaro staff only; uses the ops Teams sender instead of a webhook.' },
|
|
1719
1640
|
},
|
|
1720
1641
|
required: ['title'],
|
|
1721
1642
|
},
|
|
@@ -2061,6 +1982,27 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2061
1982
|
},
|
|
2062
1983
|
},
|
|
2063
1984
|
},
|
|
1985
|
+
{
|
|
1986
|
+
name: 'safety_policy_get',
|
|
1987
|
+
description: 'Get the safety controls policy: per control (emergency_self_harm, abuse_ladder, threat_detection, input_jailbreak, output_filter) whether it is on for AI bot and human agent, any overrides, the always-on floor, a warning when anything is off, and the acknowledgement text and version needed to turn a control off. Wraps GET SafetyPolicy.',
|
|
1988
|
+
inputSchema: { type: 'object', properties: {} },
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
name: 'safety_policy_set',
|
|
1992
|
+
description: 'Turn a safety control on, off, or back to default. Every control is ON by default. Turning one OFF is refused unless acknowledged=true AND ackTextVersion equals the current acknowledgement version (read it from safety_policy_get and show the acknowledgement text to the account owner first). This tool never accepts the acknowledgement on anyone\'s behalf. Turning on or resetting needs no acknowledgement. Wraps PUT SafetyPolicy.',
|
|
1993
|
+
inputSchema: {
|
|
1994
|
+
type: 'object',
|
|
1995
|
+
properties: {
|
|
1996
|
+
control: { type: 'string', enum: ['emergency_self_harm', 'abuse_ladder', 'threat_detection', 'input_jailbreak', 'output_filter'] },
|
|
1997
|
+
audience: { type: 'string', enum: ['ai_bot', 'human_agent'] },
|
|
1998
|
+
featureKey: { type: 'string', description: 'Optional per-feature/channel scope (chat, voice, sms, whatsapp, email). Omit for the whole account.' },
|
|
1999
|
+
enabled: { type: ['boolean', 'null'], description: 'true = on, false = off, null = clear the override (default on)' },
|
|
2000
|
+
acknowledged: { type: 'boolean', description: 'Required true when enabled=false: the account owner accepted the acknowledgement text' },
|
|
2001
|
+
ackTextVersion: { type: 'string', description: 'Required when enabled=false: the acknowledgement version from safety_policy_get' },
|
|
2002
|
+
},
|
|
2003
|
+
required: ['control', 'audience', 'enabled'],
|
|
2004
|
+
},
|
|
2005
|
+
},
|
|
2064
2006
|
{
|
|
2065
2007
|
name: 'safety_settings_test',
|
|
2066
2008
|
description: 'Send a test safety alert to the configured recipients. Wraps POST SafetyEvents/settings/test.',
|
|
@@ -2561,7 +2503,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2561
2503
|
},
|
|
2562
2504
|
},
|
|
2563
2505
|
|
|
2564
|
-
// ── Integration Diagnostics
|
|
2506
|
+
// ── Integration Diagnostics ────────────────────────────────
|
|
2565
2507
|
{
|
|
2566
2508
|
name: 'integration_activity_list',
|
|
2567
2509
|
description: 'List integration activity log entries (auth events, rate limits, config errors, AI skill calls). Fastest way to diagnose why a specific integration is failing — check this before App Insights.',
|
|
@@ -3231,7 +3173,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3231
3173
|
},
|
|
3232
3174
|
{
|
|
3233
3175
|
name: 'velaro_diagnostic_start',
|
|
3234
|
-
description: 'START HERE for any "broken", "
|
|
3176
|
+
description: 'START HERE for any "broken", "not working", "error", or "why is X failing" request about your Velaro site. Returns the diagnostic runbook: which activity log, health and report tools to check first for integration, channel, bot and routing problems.',
|
|
3235
3177
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
3236
3178
|
},
|
|
3237
3179
|
|
|
@@ -3314,6 +3256,34 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3314
3256
|
},
|
|
3315
3257
|
|
|
3316
3258
|
|
|
3259
|
+
{
|
|
3260
|
+
name: 'magento_get_sidebar_fields',
|
|
3261
|
+
description: 'Get the Magento fields shown in the agent sidebar panel for the authenticated site (empty list = out-of-box defaults). Calls messaging GET Magento/data/sidebar-fields. Site comes from the bearer token only. Read-only.',
|
|
3262
|
+
inputSchema: { type: 'object', properties: {} },
|
|
3263
|
+
},
|
|
3264
|
+
{
|
|
3265
|
+
name: 'magento_set_sidebar_fields',
|
|
3266
|
+
description: 'Set the Magento fields shown in the agent sidebar panel for the authenticated site. Partial update: omit fields to leave the saved list untouched; an empty array clears it. Site comes from the bearer token only. Calls messaging PUT Magento/data/sidebar-fields.',
|
|
3267
|
+
inputSchema: {
|
|
3268
|
+
type: 'object',
|
|
3269
|
+
properties: {
|
|
3270
|
+
fields: {
|
|
3271
|
+
type: 'array',
|
|
3272
|
+
description: 'Sidebar fields to show, in order',
|
|
3273
|
+
items: {
|
|
3274
|
+
type: 'object',
|
|
3275
|
+
properties: {
|
|
3276
|
+
fieldName: { type: 'string', description: 'Magento field name, e.g. "email"' },
|
|
3277
|
+
entity: { type: 'string', description: 'Entity the field belongs to, e.g. "customer" or "order"' },
|
|
3278
|
+
label: { type: 'string', description: 'Optional display label' },
|
|
3279
|
+
},
|
|
3280
|
+
required: ['fieldName'],
|
|
3281
|
+
},
|
|
3282
|
+
},
|
|
3283
|
+
},
|
|
3284
|
+
},
|
|
3285
|
+
},
|
|
3286
|
+
|
|
3317
3287
|
{
|
|
3318
3288
|
name: 'business_action_list',
|
|
3319
3289
|
description: 'List the shared business actions registered on the messaging API (e.g. get_customer_pricing, create_quote): name, whether it is a write, data class, freshness window, request JSON schema and the AI skill tool names that project it. One definition is served by the Velaro catalog or a connected ERP (SAP) beneath it. Site scope comes from the caller\'s auth context. Calls GET /BusinessActions.',
|
|
@@ -3480,6 +3450,29 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3480
3450
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
3481
3451
|
},
|
|
3482
3452
|
|
|
3453
|
+
{
|
|
3454
|
+
name: 'teams_phone_list_agents',
|
|
3455
|
+
description: "List the site's agents with their Microsoft Teams user Object ID mapping (agent -> Entra user), used to route inbound Teams Phone calls to the right agent. Admin only. Lives in velaro-messaging — requires VELARO_MESSAGING_API.",
|
|
3456
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
3457
|
+
},
|
|
3458
|
+
{
|
|
3459
|
+
name: 'teams_phone_set_agent',
|
|
3460
|
+
description: 'Map one Velaro agent (workspace user id) to a Microsoft Entra user Object ID (GUID) so inbound Teams Phone calls reach them. Upsert; pass "" to clear. Must be unique per site. Lives in velaro-messaging — requires VELARO_MESSAGING_API.',
|
|
3461
|
+
inputSchema: {
|
|
3462
|
+
type: 'object',
|
|
3463
|
+
properties: {
|
|
3464
|
+
agentId: { type: 'number', description: 'Velaro agent (workspace user) id from teams_phone_list_agents.' },
|
|
3465
|
+
teamsUserId: { type: 'string', description: 'Entra user Object ID (GUID). "" clears the mapping.' },
|
|
3466
|
+
},
|
|
3467
|
+
required: ['agentId', 'teamsUserId'],
|
|
3468
|
+
},
|
|
3469
|
+
},
|
|
3470
|
+
{
|
|
3471
|
+
name: 'teams_phone_delete_agent',
|
|
3472
|
+
description: "Clear one agent's Teams user mapping. Calls to that Teams user will no longer route to the agent.",
|
|
3473
|
+
inputSchema: { type: 'object', properties: { agentId: { type: 'number', description: 'Velaro agent (workspace user) id.' } }, required: ['agentId'] },
|
|
3474
|
+
},
|
|
3475
|
+
|
|
3483
3476
|
// ── Webflow (config CRUD) ──────────────────────────────────────────────────
|
|
3484
3477
|
// Lives in velaro-admin's own WebflowController.cs (this API), not velaro-messaging.
|
|
3485
3478
|
// The other 6 Webflow AI skills (webflow_list_sites, webflow_get_site,
|
|
@@ -3700,7 +3693,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3700
3693
|
},
|
|
3701
3694
|
{
|
|
3702
3695
|
name: 'usage_alerts',
|
|
3703
|
-
description: 'Plan-limit alerts for the authenticated site: every metered resource at or above its warning threshold (80%+), with used/limit, percent, level (warning | critical | limit) and whether overage is billed. Same rules as the usage-limit emails and the dashboard banner. Use to answer "are they near a limit?" or "why did they get a usage email?". Read-only. To raise a limit,
|
|
3696
|
+
description: 'Plan-limit alerts for the authenticated site: every metered resource at or above its warning threshold (80%+), with used/limit, percent, level (warning | critical | limit) and whether overage is billed. Same rules as the usage-limit emails and the dashboard banner. Use to answer "are they near a limit?" or "why did they get a usage email?". Read-only. To raise a limit, use Billing > Contact Account Manager.',
|
|
3704
3697
|
},
|
|
3705
3698
|
{
|
|
3706
3699
|
name: 'billing_contacts_list',
|
|
@@ -3827,6 +3820,8 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3827
3820
|
},
|
|
3828
3821
|
},
|
|
3829
3822
|
|
|
3823
|
+
// ── Billing agents + package builder (staff; docs/architecture/billing-agents-and-package-builder.md) ──
|
|
3824
|
+
{ name: 'package_builder_my', description: 'Customer-safe: the caller\'s own package name and what it includes. No prices or staff data.', inputSchema: { type: 'object', properties: {} } },
|
|
3830
3825
|
// ── Pricing Catalog ───────────────────────────────────────────────────────
|
|
3831
3826
|
{
|
|
3832
3827
|
name: 'pricing_catalog_list',
|
|
@@ -5333,12 +5328,12 @@ async function handleTool(name, args) {
|
|
|
5333
5328
|
return await rpc('workflow_update_node', args);
|
|
5334
5329
|
|
|
5335
5330
|
// ── Surveys ──────────────────────────────────────────────────────────
|
|
5336
|
-
// Direct messagingGet/Post/Del calls (like notifications_get
|
|
5331
|
+
// Direct messagingGet/Post/Del calls (like notifications_get above) —
|
|
5337
5332
|
// site isolation is enforced server-side by SurveysController's [Authorize]-scoped SiteId
|
|
5338
5333
|
// (ClientControllerBase), never passed by the client. Note: SurveysController's global
|
|
5339
5334
|
// [Authorize] does not accept vel_live_* service keys (only a handful of controllers opt
|
|
5340
5335
|
// into that manually) — these tools need VELARO_JWT, same requirement as the existing
|
|
5341
|
-
// notifications_get
|
|
5336
|
+
// notifications_get tool they're modeled on.
|
|
5342
5337
|
case 'ai_followups_site_get': {
|
|
5343
5338
|
const r = await messagingGet('/SiteSettings/AiFollowUps');
|
|
5344
5339
|
return JSON.stringify({ enabled: r?.enabled ?? null }, null, 2);
|
|
@@ -5809,6 +5804,19 @@ reply: ${r?.reply}`;
|
|
|
5809
5804
|
case 'agent_list':
|
|
5810
5805
|
return await rpc('agent_list', args);
|
|
5811
5806
|
|
|
5807
|
+
case 'agent_force_sign_out': {
|
|
5808
|
+
if (!Number.isInteger(args.userId) || args.userId <= 0) return 'userId is required (positive integer).';
|
|
5809
|
+
await messagingPost(`/Users/${args.userId}/ForceSignOut`, {});
|
|
5810
|
+
return `Agent ${args.userId} was signed out of every session and set to Offline. They must sign in again.`;
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
case 'agent_set_status': {
|
|
5814
|
+
if (!Number.isInteger(args.userId) || args.userId <= 0) return 'userId is required (positive integer).';
|
|
5815
|
+
if (!['Available', 'Busy', 'Away', 'Offline'].includes(args.status)) return 'status must be Available, Busy, Away or Offline.';
|
|
5816
|
+
await messagingPost('/UserStatus/admin', { userId: args.userId, status: args.status });
|
|
5817
|
+
return `Agent ${args.userId} set to ${args.status}.`;
|
|
5818
|
+
}
|
|
5819
|
+
|
|
5812
5820
|
// ── Teams ──────────────────────────────────────────────────────────────
|
|
5813
5821
|
case 'team_list':
|
|
5814
5822
|
return await rpc('team_list', args);
|
|
@@ -5883,16 +5891,6 @@ reply: ${r?.reply}`;
|
|
|
5883
5891
|
}
|
|
5884
5892
|
|
|
5885
5893
|
case 'teams_notify_card': {
|
|
5886
|
-
if ((args.mentions && args.mentions.length) || args.destination) {
|
|
5887
|
-
const r = await api('POST', '/TeamsMessaging/Card', {
|
|
5888
|
-
title: args.title,
|
|
5889
|
-
facts: (args.fields || []).map(f => ({ label: f.label, value: f.value })),
|
|
5890
|
-
actionUrl: args.buttonUrl, actionLabel: args.buttonLabel,
|
|
5891
|
-
destination: args.destination,
|
|
5892
|
-
mentions: (args.mentions || []).map(m => ({ user: m.user, name: m.name })),
|
|
5893
|
-
});
|
|
5894
|
-
return r?.sent ? `Card sent via ${r.path}${r.warnings?.length ? ' (' + r.warnings.join('; ') + ')' : ''}.` : `Send failed: ${r?.error || 'unknown error'}`;
|
|
5895
|
-
}
|
|
5896
5894
|
const webhooks = await api('GET', '/TeamsNotification');
|
|
5897
5895
|
if (!Array.isArray(webhooks) || !webhooks.length)
|
|
5898
5896
|
return 'No Teams webhooks configured. Go to Integrations → Microsoft Teams to add one, or call velaro teams create <name> <webhookUrl> via CLI.';
|
|
@@ -6386,6 +6384,21 @@ reply: ${r?.reply}`;
|
|
|
6386
6384
|
}
|
|
6387
6385
|
return JSON.stringify(await messagingPut('/SafetyEvents/settings', next), null, 2);
|
|
6388
6386
|
}
|
|
6387
|
+
case 'safety_policy_get':
|
|
6388
|
+
return JSON.stringify(await messagingGet('/SafetyPolicy'), null, 2);
|
|
6389
|
+
case 'safety_policy_set': {
|
|
6390
|
+
if (args.enabled === false && (args.acknowledged !== true || !args.ackTextVersion)) {
|
|
6391
|
+
throw new Error('Turning a safety control off requires acknowledged=true and ackTextVersion (from safety_policy_get). This tool never accepts the acknowledgement automatically.');
|
|
6392
|
+
}
|
|
6393
|
+
const body = {
|
|
6394
|
+
control: args.control,
|
|
6395
|
+
audience: args.audience,
|
|
6396
|
+
featureKey: args.featureKey ?? null,
|
|
6397
|
+
enabled: args.enabled === undefined ? null : args.enabled,
|
|
6398
|
+
...(args.enabled === false ? { acknowledged: true, ackTextVersion: args.ackTextVersion } : {}),
|
|
6399
|
+
};
|
|
6400
|
+
return JSON.stringify(await messagingPut('/SafetyPolicy', body), null, 2);
|
|
6401
|
+
}
|
|
6389
6402
|
case 'safety_settings_test':
|
|
6390
6403
|
return JSON.stringify(await messagingPost('/SafetyEvents/settings/test'), null, 2);
|
|
6391
6404
|
case 'safety_need_help':
|
|
@@ -7262,7 +7275,9 @@ reply: ${r?.reply}`;
|
|
|
7262
7275
|
}
|
|
7263
7276
|
|
|
7264
7277
|
case 'report_explain': {
|
|
7265
|
-
|
|
7278
|
+
// Packaged copy (mcp/report-explain.json, kept identical to cli/lib/report-explain.json by
|
|
7279
|
+
// scripts/validate-cli-mcp-syntax.js). The old '../cli/lib' path does not exist in a package install.
|
|
7280
|
+
const all = JSON.parse(_readFileSync(_join(_dirname(_ftu(import.meta.url)), 'report-explain.json'), 'utf8'));
|
|
7266
7281
|
const key = String(args.report ?? '').trim().toLowerCase();
|
|
7267
7282
|
if (!key) return Object.entries(all).map(([k, r]) => `${k}: ${r.name}`).join('\n');
|
|
7268
7283
|
const r = all[key];
|
|
@@ -7577,6 +7592,21 @@ reply: ${r?.reply}`;
|
|
|
7577
7592
|
return lines.join('\n');
|
|
7578
7593
|
}
|
|
7579
7594
|
|
|
7595
|
+
case 'magento_get_sidebar_fields': {
|
|
7596
|
+
const r = await messagingRequest('GET', '/Magento/data/sidebar-fields');
|
|
7597
|
+
return JSON.stringify(r, null, 2);
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7600
|
+
case 'magento_set_sidebar_fields': {
|
|
7601
|
+
const body = {};
|
|
7602
|
+
if (args.fields !== undefined) {
|
|
7603
|
+
if (!Array.isArray(args.fields)) return 'fields must be an array.';
|
|
7604
|
+
body.fields = args.fields;
|
|
7605
|
+
}
|
|
7606
|
+
const r = await messagingRequest('PUT', '/Magento/data/sidebar-fields', body);
|
|
7607
|
+
return JSON.stringify(r, null, 2);
|
|
7608
|
+
}
|
|
7609
|
+
|
|
7580
7610
|
case 'business_action_list': {
|
|
7581
7611
|
const r = await messagingRequest('GET', '/BusinessActions');
|
|
7582
7612
|
return JSON.stringify(r, null, 2);
|
|
@@ -7730,6 +7760,20 @@ reply: ${r?.reply}`;
|
|
|
7730
7760
|
return lines.join(String.fromCharCode(10));
|
|
7731
7761
|
}
|
|
7732
7762
|
|
|
7763
|
+
case 'teams_phone_list_agents': {
|
|
7764
|
+
const agents = await messagingGet('/TeamsPhone/Agents');
|
|
7765
|
+
if (!Array.isArray(agents) || agents.length === 0) return 'No agents found.';
|
|
7766
|
+
return agents.map(a => `${a.id} ${`${a.firstName ?? ''} ${a.lastName ?? ''}`.trim()} ${a.email ?? ''} ${a.teamsUserId || '(unmapped)'}`).join(String.fromCharCode(10));
|
|
7767
|
+
}
|
|
7768
|
+
case 'teams_phone_set_agent': {
|
|
7769
|
+
const r = await messagingPut(`/TeamsPhone/Agents/${encodeURIComponent(args.agentId)}`, { teamsUserId: args.teamsUserId });
|
|
7770
|
+
return r?.configured ? `Agent ${args.agentId} mapped to Teams user ${r.teamsUserId}.` : `Agent ${args.agentId} mapping cleared.`;
|
|
7771
|
+
}
|
|
7772
|
+
case 'teams_phone_delete_agent': {
|
|
7773
|
+
await messagingDel(`/TeamsPhone/Agents/${encodeURIComponent(args.agentId)}`);
|
|
7774
|
+
return `Agent ${args.agentId} mapping cleared.`;
|
|
7775
|
+
}
|
|
7776
|
+
|
|
7733
7777
|
// ── Shopify write policy (velaro-messaging /api/Shopify/WritePolicy) ───────
|
|
7734
7778
|
case 'shopify_get_write_policy': {
|
|
7735
7779
|
return JSON.stringify(await messagingGet('/Shopify/WritePolicy'), null, 2);
|
|
@@ -8149,11 +8193,13 @@ ${JSON.stringify(result, null, 2)}`;
|
|
|
8149
8193
|
lines.push('Recent payments:');
|
|
8150
8194
|
s.recentSessions.forEach(p => lines.push(` ${(p.amountCents/100).toFixed(2)} ${p.status} ${p.createdAt?.slice(0,10)}`));
|
|
8151
8195
|
}
|
|
8152
|
-
//
|
|
8153
|
-
// Use billing_plan_status_internal in staff-server.js for those.
|
|
8196
|
+
// QB sync, deferred revenue, and internal accounting details are intentionally omitted.
|
|
8154
8197
|
return lines.join('\n');
|
|
8155
8198
|
}
|
|
8156
8199
|
|
|
8200
|
+
// ── Billing agents + package builder ──────────────────────────────────────
|
|
8201
|
+
case 'package_builder_my': return JSON.stringify(await api('GET', '/PackageBuilder/my'), null, 2);
|
|
8202
|
+
|
|
8157
8203
|
// ── Pricing Catalog ───────────────────────────────────────────────────────
|
|
8158
8204
|
case 'pricing_catalog_list': {
|
|
8159
8205
|
const pkgs = await api('GET', '/Pricing/catalog');
|
|
@@ -8961,41 +9007,21 @@ ${JSON.stringify(result, null, 2)}`;
|
|
|
8961
9007
|
|
|
8962
9008
|
const DIAGNOSTIC_RUNBOOK = `# Velaro Diagnostic Runbook
|
|
8963
9009
|
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
### Step 0 — Start here every time
|
|
8967
|
-
1. \`logs_appinsights_query\` env=staging → \`exceptions | where timestamp > ago(1h) | take 20\`
|
|
8968
|
-
2. \`logs_loggly_search\` → \`tag:velaro-messaging-Staging ERROR\` (last 1h)
|
|
8969
|
-
3. \`mcp__velaro__get_diagnostic\` or \`velaro ops\` for Hangfire queue / deploy version
|
|
8970
|
-
|
|
8971
|
-
Reading code without checking logs first = debugging blind.
|
|
8972
|
-
|
|
8973
|
-
### Step 1 — Pick the right log source
|
|
8974
|
-
| Symptom | Tool | Query pattern |
|
|
8975
|
-
|---|---|---|
|
|
8976
|
-
| Exceptions / 500s in admin | logs_appinsights_query env=staging | exceptions \| where timestamp > ago(1h) \| take 20 |
|
|
8977
|
-
| Exceptions / 500s in messaging | logs_appinsights_query env=messaging-production | exceptions \| where timestamp > ago(1h) \| take 20 |
|
|
8978
|
-
| AI skill errors | logs_loggly_search | tag:velaro-messaging-Staging SkillInvoked |
|
|
8979
|
-
| Campaign / Hangfire failures | logs_loggly_search | tag:velaro-messaging-Staging Hangfire |
|
|
8980
|
-
| IVR / SignalR hub down | signalr_diagnostics instance=both hub=MessengerHub |
|
|
8981
|
-
| Prod exceptions | logs_appinsights_query env=production | exceptions \| take 20 |
|
|
8982
|
-
| Secure forms errors | logs_appinsights_query env=secure-forms | exceptions \| take 20 |
|
|
8983
|
-
| Scraper / automation errors | logs_appinsights_query env=automation-production | exceptions \| take 20 |
|
|
8984
|
-
|
|
8985
|
-
### Step 2 — SignalR alerts
|
|
8986
|
-
Alerts: SignalR-CapSaturation-PAGE, SignalR-ServiceError-PAGE, SignalR-Quota95-PAGE are wired to velaro-ci-runners (Partner Success Core Benefits sub).
|
|
8987
|
-
Use \`signalr_diagnostics hub=MessengerHub\` or \`hub=EngagementHub\` to check live connection counts.
|
|
9010
|
+
Check what Velaro already recorded before changing any configuration.
|
|
8988
9011
|
|
|
8989
|
-
|
|
8990
|
-
|
|
9012
|
+
1. Integration failures: \`integration_activity_list\` with severity=Error (filter by integration slug), then
|
|
9013
|
+
\`integration_skill_logs\` for AI skill calls that failed.
|
|
9014
|
+
2. Channel or integration connectivity: \`integration_health\` (pass run=true with a slug to probe now).
|
|
9015
|
+
3. Bot problems: \`bot_recent_errors\` and \`bot_skill_usage\`.
|
|
9016
|
+
4. Volume or service-level changes: \`kpi_snapshot\` and \`service_level_report\`.
|
|
9017
|
+
5. Routing questions: \`routing_explain_decision\` / \`routing_get_decision_log\`.
|
|
8991
9018
|
|
|
8992
|
-
|
|
8993
|
-
If all log sources are quiet → then read source code to find the bug.`;
|
|
9019
|
+
If these show nothing unusual, contact Velaro support with the time window and conversation ids you checked.`;
|
|
8994
9020
|
|
|
8995
9021
|
const PROMPTS = [
|
|
8996
9022
|
{
|
|
8997
9023
|
name: 'diagnostic_runbook',
|
|
8998
|
-
description: 'Velaro diagnostic runbook
|
|
9024
|
+
description: 'Velaro diagnostic runbook: which activity log, health and report tools to check first when something on your site is not working.',
|
|
8999
9025
|
arguments: [],
|
|
9000
9026
|
},
|
|
9001
9027
|
];
|