@velaro/mcp-server 0.6.98 → 0.6.100

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +161 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "0.6.98",
3
+ "version": "0.6.100",
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
@@ -40,6 +40,7 @@
40
40
  * VELARO_MCP_MODE=http PORT=3000 node server.js
41
41
  */
42
42
 
43
+ import { describeRawMeta, fetchRetry429 } from './log-search.js';
43
44
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
44
45
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
45
46
  import {
@@ -119,11 +120,12 @@ function parseDuration(s) {
119
120
  return n * 24 * 3600 * 1000;
120
121
  }
121
122
 
122
- async function messagingApi(path, opts = {}) {
123
+ async function messagingApi(path, opts = {}) { // opts.timeoutMs: log searches pass 40000 (server scan budget 12 s + 6 s AI search)
123
124
  if (!MESSAGING_API) throw new Error(
124
125
  'VELARO_MESSAGING_API is not configured. Add "VELARO_MESSAGING_API": "https://velaro-messaging-api-staging.azurewebsites.net" to your MCP env config.'
125
126
  );
126
- const res = await fetch(`${MESSAGING_API}${path}`, {
127
+ // 429 = the server's per-instance scan gate is full: retry honouring Retry-After (GETs only; a retried POST could repeat a write).
128
+ const init = () => ({
127
129
  method: opts.method ?? 'GET',
128
130
  headers: {
129
131
  Authorization: authHeader(),
@@ -131,8 +133,9 @@ async function messagingApi(path, opts = {}) {
131
133
  ...(opts.body ? { 'Content-Type': 'application/json' } : {}),
132
134
  },
133
135
  body: opts.body ? JSON.stringify(opts.body) : undefined,
134
- signal: AbortSignal.timeout(20000),
136
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 20000),
135
137
  });
138
+ const res = (opts.method ?? 'GET') === 'GET' ? await fetchRetry429(`${MESSAGING_API}${path}`, init) : await fetch(`${MESSAGING_API}${path}`, init());
136
139
  if (!res.ok) {
137
140
  const text = await res.text().catch(() => '');
138
141
  throw new Error(`Velaro Logger ${path} -> ${res.status}: ${text.slice(0, 300)}`);
@@ -845,12 +848,12 @@ const TOOLS = [
845
848
  {
846
849
  name: 'canned_learning_suggestions',
847
850
  description: 'List AI-proposed canned message improvements awaiting human review (learned from low ratings and never-used messages). Each row shows currentText, proposedText, the reason, the signals it learned from, and a review state (pending_review, approved, rejected, rolled_back). Intelligence Suite entitlement required.',
848
- inputSchema: { type: 'object', properties: { state: { type: 'string', description: 'pending_review (default), approved, rejected, rolled_back, or all' } } },
851
+ inputSchema: { type: 'object', properties: { state: { type: 'string', description: 'pending_review (default), approved, rejected, rolled_back, or all' }, teamId: { type: 'number', description: 'Only suggestions for canned responses on this team. Team managers only ever see their own teams.' } } },
849
852
  },
850
853
  {
851
854
  name: 'canned_learning_generate',
852
- description: 'Analyze this site canned message usage and ratings and queue AI-proposed improvements for human review. Nothing changes until approved. Capped at 5 messages per run and one run per day; text is PII-redacted before it reaches the model.',
853
- inputSchema: { type: 'object', properties: {} },
855
+ description: 'Analyze this site canned message usage and ratings and queue AI-proposed improvements for human review. Nothing changes until approved. Capped at 5 messages per run and one run per day (per team when teamId is given); text is PII-redacted before it reaches the model. Team managers must pass a teamId they manage.',
856
+ inputSchema: { type: 'object', properties: { teamId: { type: 'number', description: 'Only generate for canned responses on this team' } } },
854
857
  },
855
858
  {
856
859
  name: 'canned_learning_approve',
@@ -865,6 +868,11 @@ const TOOLS = [
865
868
  {
866
869
  name: 'canned_learning_edit_summary',
867
870
  description: 'Which canned messages agents edit most before sending (last 90 days): canned response id, shortcut, edit count and average similarity of the sent text to the original. This is the edited-before-send evidence the weekly canned message learning job uses. Intelligence Suite entitlement required.',
871
+ inputSchema: { type: 'object', properties: { teamId: { type: 'number', description: 'Only canned responses on this team' } } },
872
+ },
873
+ {
874
+ name: 'canned_learning_teams',
875
+ description: 'Teams the caller can review canned message learning for: every site team for a site supervisor, only managed teams for a team manager. Use the ids as teamId on the other canned_learning tools.',
868
876
  inputSchema: { type: 'object', properties: {} },
869
877
  },
870
878
  {
@@ -2152,6 +2160,62 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2152
2160
  required: ['fromIndex', 'toIndex'],
2153
2161
  },
2154
2162
  },
2163
+ {
2164
+ name: 'safety_events_list',
2165
+ description: "List the site's safety events (emergency, emergency_unconfirmed, therapy_setting_change, fail_closed, output_guardrail_block, visitor_report, off_topic_attempt). Needs the EnableTroubleshootingKnowledge entitlement. Wraps GET SafetyEvents; the site comes from the caller auth, never an argument.",
2166
+ inputSchema: {
2167
+ type: 'object',
2168
+ properties: {
2169
+ from: { type: 'string', description: 'ISO start date/time' },
2170
+ to: { type: 'string', description: 'ISO end date/time' },
2171
+ type: { type: 'string', description: 'One event type' },
2172
+ status: { type: 'string', enum: ['open', 'acknowledged'] },
2173
+ take: { type: 'number', description: 'Page size, default 25' },
2174
+ skip: { type: 'number', description: 'Rows to skip, default 0' },
2175
+ },
2176
+ },
2177
+ },
2178
+ {
2179
+ name: 'safety_events_summary',
2180
+ description: 'Per-day and total safety event counts by type for the last N days (default 7). Wraps GET SafetyEvents/summary.',
2181
+ inputSchema: { type: 'object', properties: { days: { type: 'number', description: 'Default 7, e.g. 30' } } },
2182
+ },
2183
+ {
2184
+ name: 'safety_event_ack',
2185
+ description: 'Mark one safety event as reviewed. Wraps POST SafetyEvents/{id}/acknowledge.',
2186
+ inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
2187
+ },
2188
+ {
2189
+ name: 'safety_settings_get',
2190
+ description: 'Get the safety alert settings: enabled, emailRecipients, smsRecipients, immediateTypes, digest (off|daily|weekly), threshold {count, windowMinutes}. Wraps GET SafetyEvents/settings.',
2191
+ inputSchema: { type: 'object', properties: {} },
2192
+ },
2193
+ {
2194
+ name: 'safety_settings_set',
2195
+ description: 'Change the safety alert settings. Only the fields you pass change; the rest keep their current value. Wraps PUT SafetyEvents/settings (reads the current settings first).',
2196
+ inputSchema: {
2197
+ type: 'object',
2198
+ properties: {
2199
+ enabled: { type: 'boolean' },
2200
+ emailRecipients: { type: 'array', items: { type: 'string' } },
2201
+ smsRecipients: { type: 'array', items: { type: 'string' }, description: 'E.164 numbers, e.g. +15551234567' },
2202
+ immediateTypes: { type: 'array', items: { type: 'string' }, description: 'Event types that alert right away' },
2203
+ digest: { type: 'string', enum: ['off', 'daily', 'weekly'] },
2204
+ thresholdCount: { type: 'number', description: 'Alert when this many events happen inside the window. 0 turns it off.' },
2205
+ thresholdWindowMinutes: { type: 'number', description: '1 to 10080' },
2206
+ },
2207
+ },
2208
+ },
2209
+ {
2210
+ name: 'safety_settings_test',
2211
+ description: 'Send a test safety alert to the configured recipients. Wraps POST SafetyEvents/settings/test.',
2212
+ inputSchema: { type: 'object', properties: {} },
2213
+ },
2214
+ {
2215
+ name: 'safety_need_help',
2216
+ description: 'Tell the Velaro team the bot is going off the rails and you need help now. Wraps POST SafetyEvents/need-help.',
2217
+ inputSchema: { type: 'object', properties: { message: { type: 'string', description: 'What is happening' } }, required: ['message'] },
2218
+ },
2155
2219
  {
2156
2220
  name: 'index_move_status',
2157
2221
  description: 'Status of one move (moveId) or the recent moves (omit moveId): status, per-document progress, canResume/canAbort/canUndo. Wraps GET IndexMove/Moves[/{id}].',
@@ -2864,6 +2928,25 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2864
2928
  },
2865
2929
  },
2866
2930
 
2931
+ // ── Chat Window launcher copy (per-page launcher label + greeting) ─────────
2932
+ {
2933
+ name: 'chat_window_list',
2934
+ description: 'List chat windows for this site with the launcher label (launcherText) and greeting. Point a Select Chat Window workflow node or Determine Experience Bundle rule at a window to show its label and greeting on specific pages.',
2935
+ inputSchema: { type: 'object', properties: {}, required: [] },
2936
+ },
2937
+ {
2938
+ name: 'chat_window_set_launcher_copy',
2939
+ description: 'Set the launcher label (the text pill on the chat button, max 60 chars) and/or the opening greeting (max 256 chars) on one chat window. Omitted fields are left unchanged; an empty string clears the field and the widget falls back to its default. For per-page copy, create one chat window per page type (and per language) and select it with a Select Chat Window workflow node.',
2940
+ inputSchema: {
2941
+ type: 'object',
2942
+ properties: {
2943
+ id: { type: 'number', description: 'Chat window ID (see chat_window_list)' },
2944
+ launcherText: { type: 'string', description: 'Launcher label, e.g. "Troubleshoot my device"' },
2945
+ greeting: { type: 'string', description: 'Opening greeting shown when the window opens' },
2946
+ },
2947
+ required: ['id'],
2948
+ },
2949
+ },
2867
2950
  // ── Page Widget Containers ────────────────────────────────────────────────
2868
2951
  {
2869
2952
  name: 'widget_container_list',
@@ -6340,15 +6423,17 @@ async function handleTool(name, args) {
6340
6423
  if (args.confirm) return await messagingPost('/ChatIntelligenceTopics/analysis', { confirm: true });
6341
6424
  return { estimate: await messagingGet('/ChatIntelligenceTopics/analysis/estimate'), cached: await messagingGet('/ChatIntelligenceTopics/analysis') };
6342
6425
  case 'canned_learning_suggestions':
6343
- return await messagingGet(`/CannedLearning/suggestions?state=${encodeURIComponent(args.state ?? 'pending_review')}`);
6426
+ return await messagingGet(`/CannedLearning/suggestions?state=${encodeURIComponent(args.state ?? 'pending_review')}${args.teamId != null ? `&teamId=${encodeURIComponent(args.teamId)}` : ''}`);
6344
6427
  case 'canned_learning_generate':
6345
- return await messagingPost('/CannedLearning/generate', {});
6428
+ return await messagingPost('/CannedLearning/generate', { teamId: args.teamId ?? null });
6429
+ case 'canned_learning_teams':
6430
+ return await messagingGet('/CannedLearning/teams');
6346
6431
  case 'canned_learning_approve':
6347
6432
  return await messagingPost(`/CannedLearning/suggestions/${args.suggestionId}/approve`, { editedText: args.editedText ?? null });
6348
6433
  case 'canned_learning_reject':
6349
6434
  return await messagingPost(`/CannedLearning/suggestions/${args.suggestionId}/reject`, {});
6350
6435
  case 'canned_learning_edit_summary':
6351
- return await messagingGet('/CannedLearning/edit-events');
6436
+ return await messagingGet(`/CannedLearning/edit-events${args.teamId != null ? `?teamId=${encodeURIComponent(args.teamId)}` : ''}`);
6352
6437
  case 'canned_learning_record_edit':
6353
6438
  return await messagingPost('/CannedLearning/edit-events', { cannedResponseId: args.cannedResponseId, finalText: args.finalText });
6354
6439
  case 'canned_learning_rollback':
@@ -7569,9 +7654,18 @@ async function handleTool(name, args) {
7569
7654
  const from1h = new Date(Date.now() - 3600000).toISOString();
7570
7655
  const velaroLoggerP = safeJson(async () => {
7571
7656
  if (!MESSAGING_API) return { _error: 'VELARO_MESSAGING_API not set — add it to MCP env config' };
7572
- const q = encodeURIComponent('ERROR|WF-AI-ROUTE|PERF-SLOW');
7573
- const res = await messagingApi(`/superadmin/logs/search?q=${q}&from=${encodeURIComponent(from1h)}&take=30&regex=true`);
7574
- return extractLogEntries(res);
7657
+ // Two honest reads (Opus review 2026-10-14): ERRORS use the server-side level=ERROR filter; PATTERNS are the WF-AI-ROUTE / PERF-SLOW regex (no level).
7658
+ const f = encodeURIComponent(from1h);
7659
+ // One at a time: the server runs at most 2 exact scans per instance.
7660
+ const errRes = await messagingApi(`/superadmin/logs/search?level=ERROR&from=${f}&take=30`, { timeoutMs: 40000 });
7661
+ const patRes = await messagingApi(`/superadmin/logs/search?q=${encodeURIComponent('WF-AI-ROUTE|PERF-SLOW')}&regex=true&from=${f}&take=30`, { timeoutMs: 40000 });
7662
+ const byId = new Map();
7663
+ for (const e of [...extractLogEntries(errRes), ...extractLogEntries(patRes)]) byId.set(e.id ?? `${e.timestamp}|${e.message}`, e);
7664
+ const found = [...byId.values()].sort((x, y) => (Date.parse(y.timestamp ?? y.LogTimestamp) || 0) - (Date.parse(x.timestamp ?? x.LogTimestamp) || 0));
7665
+ const verified = (r) => !!(r && !Array.isArray(r) && r.truncated === false);
7666
+ found._verified = verified(errRes) && verified(patRes);
7667
+ found._note = `errors: ${describeRawMeta(errRes)} | patterns: ${describeRawMeta(patRes)}`;
7668
+ return found;
7575
7669
  });
7576
7670
 
7577
7671
  // ── Layer 4b: Loggly ERROR + SLOW + AI-routing logs (last 1h) — SECONDARY (migrating away)
@@ -7647,9 +7741,11 @@ async function handleTool(name, args) {
7647
7741
  if (velaroLogs?._error) {
7648
7742
  out.push(section('Velaro Logger (1h) [PRIMARY]', [` ⚠ ${velaroLogs._error}`]));
7649
7743
  } else if (!Array.isArray(velaroLogs) || !velaroLogs.length) {
7650
- out.push(section('Velaro Logger (1h) [PRIMARY]', [' ✅ No errors, slow responses, or AI routing events']));
7744
+ out.push(section('Velaro Logger (1h) [PRIMARY]', velaroLogs?._verified
7745
+ ? [` ✅ No errors, slow responses, or AI routing events (${velaroLogs._note})`]
7746
+ : [` ⚠ No matches, but the scan is NOT verified complete: ${velaroLogs?._note ?? 'no scan metadata'}. Use velaro_logs_search before concluding all is well.`]));
7651
7747
  } else {
7652
- out.push(section(`Velaro Logger (1h) [PRIMARY] — ${velaroLogs.length} entries`, velaroLogs.map(fmtVelaroEntry)));
7748
+ out.push(section(`Velaro Logger (1h) [PRIMARY] — ${velaroLogs.length} entries${velaroLogs._verified ? '' : ` (scan not verified complete: ${velaroLogs._note})`}`, velaroLogs.map(fmtVelaroEntry)));
7653
7749
  }
7654
7750
 
7655
7751
  // ── Loggly (secondary — being phased out; check if Velaro Logger is empty/errored)
@@ -7755,6 +7851,35 @@ async function handleTool(name, args) {
7755
7851
  const r = await messagingPost('/IndexMove/Split/Execute', body);
7756
7852
  return JSON.stringify(r, null, 2);
7757
7853
  }
7854
+ case 'safety_events_list': {
7855
+ const q = new URLSearchParams();
7856
+ for (const k of ['from', 'to', 'type', 'status']) if (args[k]) q.set(k, String(args[k]));
7857
+ q.set('take', String(args.take ?? 25));
7858
+ q.set('skip', String(args.skip ?? 0));
7859
+ return JSON.stringify(await messagingGet(`/SafetyEvents?${q}`), null, 2);
7860
+ }
7861
+ case 'safety_events_summary':
7862
+ return JSON.stringify(await messagingGet(`/SafetyEvents/summary?days=${Number(args.days) || 7}`), null, 2);
7863
+ case 'safety_event_ack':
7864
+ return JSON.stringify(await messagingPost(`/SafetyEvents/${encodeURIComponent(args.id)}/acknowledge`), null, 2);
7865
+ case 'safety_settings_get':
7866
+ return JSON.stringify(await messagingGet('/SafetyEvents/settings'), null, 2);
7867
+ case 'safety_settings_set': {
7868
+ const cur = (await messagingGet('/SafetyEvents/settings')) ?? {};
7869
+ const next = { ...cur };
7870
+ for (const k of ['enabled', 'emailRecipients', 'smsRecipients', 'immediateTypes', 'digest']) if (args[k] !== undefined) next[k] = args[k];
7871
+ if (args.thresholdCount !== undefined || args.thresholdWindowMinutes !== undefined) {
7872
+ next.threshold = {
7873
+ count: args.thresholdCount ?? cur.threshold?.count ?? 0,
7874
+ windowMinutes: args.thresholdWindowMinutes ?? cur.threshold?.windowMinutes ?? 60,
7875
+ };
7876
+ }
7877
+ return JSON.stringify(await messagingPut('/SafetyEvents/settings', next), null, 2);
7878
+ }
7879
+ case 'safety_settings_test':
7880
+ return JSON.stringify(await messagingPost('/SafetyEvents/settings/test'), null, 2);
7881
+ case 'safety_need_help':
7882
+ return JSON.stringify(await messagingPost('/SafetyEvents/need-help', { message: args.message }), null, 2);
7758
7883
  case 'index_move_status': {
7759
7884
  const r = await messagingGet(args.moveId
7760
7885
  ? `/IndexMove/Moves/${encodeURIComponent(args.moveId)}`
@@ -8611,9 +8736,11 @@ async function handleTool(name, args) {
8611
8736
  if (args.environment) params.set('environment', args.environment);
8612
8737
  if (args.correlationId) params.set('correlationId', args.correlationId);
8613
8738
  if (args.regex) params.set('regex', 'true');
8614
- const entries = extractLogEntries(await messagingApi(`/superadmin/logs/search?${params}`));
8615
- if (!entries.length) return 'No results.';
8616
- const lines = [`${entries.length} result(s) from ${from.toISOString().slice(0, 16)} UTC\n`];
8739
+ const resp = await messagingApi(`/superadmin/logs/search?${params}`, { timeoutMs: 40000 });
8740
+ const entries = extractLogEntries(resp);
8741
+ const meta = describeRawMeta(resp); // searchPath/scannedRows/truncated: a 0 or short list is never ambiguous
8742
+ if (!entries.length) return `No results. ${meta}`;
8743
+ const lines = [`${entries.length} result(s) from ${from.toISOString().slice(0, 16)} UTC. ${meta}\n`];
8617
8744
  for (const e of entries) {
8618
8745
  const ts = new Date(e.timestamp ?? e.LogTimestamp).toISOString().replace('T', ' ').slice(0, 19);
8619
8746
  const lvl = (e.Level ?? e.level ?? '?').padEnd(5);
@@ -8707,6 +8834,23 @@ async function handleTool(name, args) {
8707
8834
  ).join('\n');
8708
8835
  }
8709
8836
 
8837
+ case 'chat_window_list': {
8838
+ const r = (await messagingGet('/ChatWindow/list')) ?? [];
8839
+ return JSON.stringify(r.map((w) => ({
8840
+ id: w.id, name: w.name, language: w.languageConfig?.language,
8841
+ launcherText: w.styleConfig?.launcherText ?? null, greeting: w.languageConfig?.greeting ?? null,
8842
+ })), null, 2);
8843
+ }
8844
+
8845
+ case 'chat_window_set_launcher_copy': {
8846
+ if (!args.id) throw new Error('id is required');
8847
+ const body = {};
8848
+ if (args.launcherText !== undefined) body.launcherText = args.launcherText;
8849
+ if (args.greeting !== undefined) body.greeting = args.greeting;
8850
+ if (!Object.keys(body).length) throw new Error('provide launcherText and/or greeting');
8851
+ return JSON.stringify(await messagingPut(`/ChatWindow/${args.id}/launcher-copy`, body), null, 2);
8852
+ }
8853
+
8710
8854
  case 'widget_container_get': {
8711
8855
  const c = await api('GET', `/PageWidgetContainers/${args.id}`);
8712
8856
  return JSON.stringify(c, null, 2);