@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/server.js CHANGED
@@ -40,7 +40,7 @@
40
40
  * VELARO_MCP_MODE=http PORT=3000 node server.js
41
41
  */
42
42
 
43
- import { buildRawLogQuery, describeRawMeta, fetchRetry429, resolveLogWindow } from './log-search.js';
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
- // Default was 'https://help.velaro.com'. Renamed to the recognizable admin API hostname for
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(`Velaro Logger ${path} -> ${res.status}: ${text.slice(0, 300)}`);
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, existsSync as _existsSync } from 'node:fs';
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 (Velaro staff) ────────────────────────────────
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", "fix", "not working", "error", or "why is X failing" request. Returns the Velaro diagnostic runbook: which log tool to check first, query patterns for common symptoms (exceptions, AI skill errors, Hangfire failures, SignalR hub down, campaign issues), and the rule "check logs before reading code." Call this before touching any source file.',
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, a customer uses Billing > Contact Account Manager; staff use support_set_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/ai_config_staff_get above) —
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/ai_config_staff_get tools they're modeled on.
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
- const all = JSON.parse(_readFileSync(_join(_dirname(_ftu(import.meta.url)), '..', 'cli', 'lib', 'report-explain.json'), 'utf8'));
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
- // Note: QB sync, deferred revenue, and internal accounting details are omitted here.
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
- ## ALWAYS DO THIS FIRST before reading any code on a "broken" / "fix" / "not working" request
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
- ### Step 3 — Integration issues
8990
- Use \`mcp__velaro__get_conversations\` or activity log endpoint \`/Integration/Logs/{slug}?severity=Error\` before digging into skill files.
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
- ### Step 4 — Only read code if logs are clean
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 — which log tool to use first, query patterns for common symptoms, SignalR alert reference. ALWAYS consult this before reading code on any "broken" or "fix" request.',
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
  ];