@velaro/mcp-server 0.6.90 → 0.6.92

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 +338 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "0.6.90",
3
+ "version": "0.6.92",
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
@@ -822,6 +822,81 @@ const TOOLS = [
822
822
  },
823
823
  },
824
824
  },
825
+ {
826
+ name: 'canned_learning_suggestions',
827
+ 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.',
828
+ inputSchema: { type: 'object', properties: { state: { type: 'string', description: 'pending_review (default), approved, rejected, rolled_back, or all' } } },
829
+ },
830
+ {
831
+ name: 'canned_learning_generate',
832
+ 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.',
833
+ inputSchema: { type: 'object', properties: {} },
834
+ },
835
+ {
836
+ name: 'canned_learning_approve',
837
+ description: 'Approve a suggestion: replaces the live canned message with the proposed text (or editedText if given). The original is snapshotted so canned_learning_rollback can restore it.',
838
+ inputSchema: { type: 'object', properties: { suggestionId: { type: 'number' }, editedText: { type: 'string', description: 'Optional human-edited replacement' } }, required: ['suggestionId'] },
839
+ },
840
+ {
841
+ name: 'canned_learning_reject',
842
+ description: 'Reject a pending canned message suggestion. The live message is untouched.',
843
+ inputSchema: { type: 'object', properties: { suggestionId: { type: 'number' } }, required: ['suggestionId'] },
844
+ },
845
+ {
846
+ name: 'canned_learning_edit_summary',
847
+ 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.',
848
+ inputSchema: { type: 'object', properties: {} },
849
+ },
850
+ {
851
+ name: 'canned_learning_record_edit',
852
+ description: 'Record that an agent edited a canned response before sending. The sent text is PII-redacted and length-capped server-side; the site comes from the session. Rows are capped per site and expire after 90 days.',
853
+ inputSchema: { type: 'object', properties: { cannedResponseId: { type: 'number' }, finalText: { type: 'string', description: 'The text the agent actually sent' } }, required: ['cannedResponseId', 'finalText'] },
854
+ },
855
+ {
856
+ name: 'canned_learning_rollback',
857
+ description: 'Roll back an approved suggestion, restoring the original canned message text.',
858
+ inputSchema: { type: 'object', properties: { suggestionId: { type: 'number' }, notes: { type: 'string' } }, required: ['suggestionId'] },
859
+ },
860
+ {
861
+ name: 'chat_topics_get',
862
+ description: 'Chat Intelligence topics for this site: the active topic list (hint for the AI, not a rulebook), industry templates, groups, merge/rename mapping and limits. Intelligence Suite entitlement required.',
863
+ inputSchema: { type: 'object', properties: {} },
864
+ },
865
+ {
866
+ name: 'chat_topics_set',
867
+ description: 'Replace the active topic list (max 60 topics, 60 chars each, duplicates removed). Used for new classifications only; existing history is untouched. Optionally load a template first with industry.',
868
+ inputSchema: { type: 'object', properties: { topics: { type: 'array', items: { type: 'string' } }, industry: { type: 'string', description: 'Load a template instead: insurance, healthcare, retail, ecommerce, financial, saas, government, manufacturing' } } },
869
+ },
870
+ {
871
+ name: 'chat_topics_edit',
872
+ description: 'Merge, rename, retire or group topics. Applied when reports are read, so history follows the new names with zero AI cost. op = merge|rename|retire|group; from = topic names; to = target (merge/rename); group = group name (empty clears).',
873
+ inputSchema: { type: 'object', properties: { op: { type: 'string' }, from: { type: 'array', items: { type: 'string' } }, to: { type: 'string' }, group: { type: 'string' } }, required: ['op', 'from'] },
874
+ },
875
+ {
876
+ name: 'chat_topics_group_suggestions',
877
+ description: 'Suggested groups for the current topics, computed from topic names and counts with no AI call.',
878
+ inputSchema: { type: 'object', properties: { days: { type: 'number' } } },
879
+ },
880
+ {
881
+ name: 'chat_topics_suggestions',
882
+ description: 'Suggested topics: the short topic the AI proposed for chats that fit nothing on the list, with counts and example conversation ids.',
883
+ inputSchema: { type: 'object', properties: { days: { type: 'number' } } },
884
+ },
885
+ {
886
+ name: 'chat_topics_resolve_suggestion',
887
+ description: 'Accept (adds to the list), merge or rename into an existing topic (target), or dismiss a suggested topic. History follows with no AI cost.',
888
+ inputSchema: { type: 'object', properties: { suggestion: { type: 'string' }, action: { type: 'string', description: 'accept|merge|rename|dismiss' }, target: { type: 'string' } }, required: ['suggestion', 'action'] },
889
+ },
890
+ {
891
+ name: 'chat_topics_reclassify',
892
+ description: 'Optional: re-run the classifier on older chats. Call with confirm omitted/false to get the cost estimate (scope other = only unlisted chats, all = everything; hard cap 500). Resend with confirm=true to queue it. Never needed for merge/rename/group.',
893
+ inputSchema: { type: 'object', properties: { scope: { type: 'string' }, days: { type: 'number' }, max: { type: 'number' }, confirm: { type: 'boolean' } } },
894
+ },
895
+ {
896
+ name: 'chat_topics_analysis',
897
+ description: 'Optional one-call analysis (cheap model, aggregate counts only, PII redacted, cached, one run per day per site) that proposes merges/renames/groups. confirm omitted/false returns the estimate and any cached result; confirm=true runs it. Then call with proposalId + accept to apply or reject one proposal.',
898
+ inputSchema: { type: 'object', properties: { confirm: { type: 'boolean' }, proposalId: { type: 'string' }, accept: { type: 'boolean' } } },
899
+ },
825
900
  {
826
901
  name: 'chat_intelligence_recommendations',
827
902
  description: 'List the active (non-dismissed) AI-generated improvement recommendations for this site — widget availability, bot gaps, missed-chat patterns, content-gap drafts. Each row includes id, type, priority, title, body, supportingDataBlob, plus lifecycle fields (appliedAt, appliedArtifactRef, rolledBackAt).',
@@ -2976,6 +3051,26 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2976
3051
  },
2977
3052
  },
2978
3053
  },
3054
+ {
3055
+ name: 'enterprise_reports',
3056
+ description: 'Enterprise Reports package (enterprise-internal, default OFF; every report returns 403 when the site does not hold that package item). report: program_breakdown (compare websites, teams, platforms, devices or entry pages: volume, missed, transfers, wait, first reply, QA score and coverage, sentiment), agent_scorecard (per agent by name: resolution, transfers, handle time, AI score, supervisor-reviewed score, sentiment impact), qa_coverage (share of resolved chats AI-scored and supervisor-reviewed by week and program), coaching_queue (low-scoring chats nobody reviewed, per agent), sentiment_breakdown (by website, team, agent or topic with change vs prior period), topic_radar (topics by volume, growth, transfer rate), weekly_brief (preview of the Monday brief sections), weekly_brief_send_test (emails this week\'s brief to the signed-in user). Scoped to the caller site.',
3057
+ inputSchema: {
3058
+ type: 'object',
3059
+ properties: {
3060
+ report: { type: 'string', enum: ['program_breakdown', 'agent_scorecard', 'qa_coverage', 'coaching_queue', 'sentiment_breakdown', 'topic_radar', 'weekly_brief', 'weekly_brief_send_test'] },
3061
+ from: { type: 'string', description: 'yyyy-MM-dd start (default: 30 days ago).' },
3062
+ to: { type: 'string', description: 'yyyy-MM-dd end, inclusive (default: today).' },
3063
+ groupBy: { type: 'string', description: 'program_breakdown: website|team|platform|device|entrypage. sentiment_breakdown: website|team|agent|theme.' },
3064
+ deploymentId: { type: 'number', description: 'Optional website (deployment) ID.' },
3065
+ teamId: { type: 'number', description: 'Optional team ID.' },
3066
+ threshold: { type: 'number', description: 'coaching_queue: queue chats scored below this (1-100; default = the rubric low-score threshold or 60).' },
3067
+ perAgent: { type: 'number', description: 'coaching_queue: chats listed per agent (default 5).' },
3068
+ limit: { type: 'number', description: 'topic_radar: max topics (default 25).' },
3069
+ days: { type: 'number', description: 'weekly_brief: days to cover (default 7).' },
3070
+ },
3071
+ required: ['report'],
3072
+ },
3073
+ },
2979
3074
  {
2980
3075
  name: 'report_backfill',
2981
3076
  description: 'Backfill report data for the caller site. Preview by default (reports "would fill N", writes nothing); apply=true writes. Idempotent. type "csat" fills missing survey ratings (SurveySubmission.Rating) from existing data; it does NOT touch ticket CSAT. Site managers only; scoped to the caller site.',
@@ -3156,6 +3251,10 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3156
3251
  voiceId: { type: 'string', description: 'ElevenLabs voice ID override. Omit to use the site default.' },
3157
3252
  showCallMeOption: { type: 'boolean', description: 'Show the "Call me instead" option (requires EnableClickToCall).' },
3158
3253
  callbackConfirmWindowMinutes: { type: 'number', description: 'Minutes to confirm a callback request before it expires. Default 10.' },
3254
+ displayName: { type: 'string', description: 'Visitor-facing avatar name shown on the badge (e.g. "Moshky"). Empty string resets to "AI".' },
3255
+ showBadge: { type: 'boolean', description: 'Show the name badge over the avatar video. Default true.' },
3256
+ sizePx: { type: 'number', description: 'Max avatar panel width in px (160-640, square). Default 360.' },
3257
+ allowVisitorCollapse: { type: 'boolean', description: 'Let visitors hide or collapse the avatar panel (up/down arrow and X). Default true.' },
3159
3258
  maxAvatarMinutesPerMonth: { type: 'number', description: 'Monthly avatar video minutes cap for the site. Whole number 1-100000; never unlimited (0/negative rejected).' },
3160
3259
  maxAvatarTtsChars: { type: 'number', description: 'Monthly avatar TTS character cap (avatar_tts_chars) for the site. Whole number 1-100000000; never unlimited (0/negative rejected). Overage pricing is a platform-wide ladder, not per site.' },
3161
3260
  },
@@ -3371,7 +3470,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3371
3470
  },
3372
3471
  {
3373
3472
  name: 'email_inbox_get_status',
3374
- description: 'Check global Gmail OAuth/Domain-Wide-Delegation/Pub-Sub configuration presence (gmail only — these are global App Settings, not per-site). For per-mailbox connection state (either provider) use email_inbox_list_mailboxes instead. Velaro staff only.',
3473
+ description: 'Check global Gmail OAuth/Domain-Wide-Delegation/Pub-Sub configuration presence AND prove the OAuth client id/secret are a matching pair (credentialPair) plus duplicate App Setting spellings (Gmail:X vs Gmail__X) (gmail only — these are global App Settings, not per-site). For per-mailbox connection state (either provider) use email_inbox_list_mailboxes instead. Velaro staff only.',
3375
3474
  inputSchema: {
3376
3475
  type: 'object',
3377
3476
  properties: {
@@ -3380,6 +3479,33 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3380
3479
  required: ['siteId'],
3381
3480
  },
3382
3481
  },
3482
+ {
3483
+ name: 'email_inbox_resubscribe_outlook',
3484
+ description: 'Recreate the Microsoft Graph inbound-mail subscription for a connected Outlook mailbox. A mailbox with graphSubscriptionId null (see email_inbox_list_mailboxes) shows "connected" but never receives email - this repairs it immediately. Velaro staff only.',
3485
+ inputSchema: {
3486
+ type: 'object',
3487
+ properties: {
3488
+ siteId: { type: 'number', description: 'Site ID' },
3489
+ mailboxId: { type: 'number', description: 'Outlook mailbox integration ID from email_inbox_list_mailboxes' },
3490
+ },
3491
+ required: ['siteId', 'mailboxId'],
3492
+ },
3493
+ },
3494
+ {
3495
+ name: 'site_integration_logs',
3496
+ description: "Read ANY site's integration Activity Log (same rows as the admin Activity Log tab), optionally filtered by integration slug (e.g. OutlookInbox, GmailInbox, NetSuite), severity and message search. Use this first when diagnosing a customer/test-site integration. Velaro staff only.",
3497
+ inputSchema: {
3498
+ type: 'object',
3499
+ properties: {
3500
+ siteId: { type: 'number', description: 'Site ID' },
3501
+ slug: { type: 'string', description: 'Integration slug, e.g. OutlookInbox' },
3502
+ severity: { type: 'string', enum: ['Info', 'Warning', 'Error'], description: 'Optional severity filter' },
3503
+ search: { type: 'string', description: 'Optional substring match on message / additional info' },
3504
+ take: { type: 'number', description: 'Max rows (default 50, max 200)' },
3505
+ },
3506
+ required: ['siteId'],
3507
+ },
3508
+ },
3383
3509
  {
3384
3510
  name: 'email_inbox_set_team',
3385
3511
  description: 'Reassign the team a connected Gmail/Outlook mailbox routes inbound mail to, or clear it back to the site default/catch-all (teamId: 0). Velaro staff only.',
@@ -4396,6 +4522,11 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
4396
4522
  description: 'List all registered EntitlementFeature keys with their display names and categories. Useful for finding the right featureKey before setting overrides. Superadmin only.',
4397
4523
  inputSchema: { type: 'object', properties: {} },
4398
4524
  },
4525
+ {
4526
+ name: 'entitlement_enterprise_reports_package',
4527
+ description: 'Show the Enterprise Reports package (enterprise-internal, default OFF): every item with its feature key, what it builds on, and, when siteId is given, whether it is ON for that site. Superadmin only.',
4528
+ inputSchema: { type: 'object', properties: { siteId: { type: 'number', description: 'Optional site ID to show which items are on' } } },
4529
+ },
4399
4530
  {
4400
4531
  name: 'entitlement_set_override',
4401
4532
  description: 'Set a per-site feature override — enable or disable a specific feature for one site, independent of their plan. Superadmin only. Use entitlement_list_features to find valid featureKey values.',
@@ -4849,6 +4980,52 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
4849
4980
  description: 'List candidate applications submitted via the Recruiting Bot for this site (name, email, phone, role interest, work authorization, availability, submitted timestamp). Never contains EEOC self-ID data.',
4850
4981
  inputSchema: { type: 'object', properties: {}, required: [] },
4851
4982
  },
4983
+ {
4984
+ name: 'recruiting_ats_failures',
4985
+ description: 'List candidate applications whose ATS write-back is Pending, Failed or Abandoned (status, attempts, last error, next retry). Requires EnableRecruitingBot.',
4986
+ inputSchema: { type: 'object', properties: {}, required: [] },
4987
+ },
4988
+ {
4989
+ name: 'recruiting_ats_retry',
4990
+ description: 'Retry the ATS write-back for one candidate application right now (resets the attempt counter). Returns the new sync status.',
4991
+ inputSchema: { type: 'object', properties: { applicationId: { type: 'number' } }, required: ['applicationId'] },
4992
+ },
4993
+ {
4994
+ name: 'recruiting_apply_settings',
4995
+ description: 'Set public apply form options for this site (apply-if-present): publicApplyEnabled toggles the public apply endpoint; autoCreateCaseTypeId picks the case type auto-created per application (0 clears).',
4996
+ inputSchema: {
4997
+ type: 'object',
4998
+ properties: {
4999
+ publicApplyEnabled: { type: 'boolean' },
5000
+ autoCreateCaseTypeId: { type: 'number', description: 'Case type id; 0 clears the auto-create link' },
5001
+ },
5002
+ required: [],
5003
+ },
5004
+ },
5005
+ {
5006
+ name: 'recruiting_bot_settings',
5007
+ description: 'Get or set the recruiting bot customer controls (apply-if-present; no fields = read): pageContextEnabled (use the job from the host page Velaro context call), revealPostedDate, revealDescription, revealApplyLink, publicApplyEnabled, applyMode (both|link|inproduct), maxResults (1-10), greeting (max 500), tone (friendly|professional|concise).',
5008
+ inputSchema: {
5009
+ type: 'object',
5010
+ properties: {
5011
+ pageContextEnabled: { type: 'boolean' },
5012
+ revealPostedDate: { type: 'boolean' },
5013
+ revealDescription: { type: 'boolean' },
5014
+ revealApplyLink: { type: 'boolean' },
5015
+ publicApplyEnabled: { type: 'boolean' },
5016
+ applyMode: { type: 'string', enum: ['both', 'link', 'inproduct'] },
5017
+ maxResults: { type: 'number' },
5018
+ greeting: { type: 'string' },
5019
+ tone: { type: 'string', enum: ['friendly', 'professional', 'concise'] },
5020
+ },
5021
+ required: [],
5022
+ },
5023
+ },
5024
+ {
5025
+ name: 'recruiting_download_resume',
5026
+ description: 'Download a candidate application resume. Returns { fileName, contentType, sizeBytes, base64 }. Files over 5 MB are refused; use the CLI (velaro recruiting applications resume) for those.',
5027
+ inputSchema: { type: 'object', properties: { applicationId: { type: 'number' } }, required: ['applicationId'] },
5028
+ },
4852
5029
  // Recruiting/Applications/ExportEeoc (voluntary self-ID CSV export) is intentionally NOT
4853
5030
  // exposed as an MCP tool -- it returns sensitive EEOC/OFCCP self-identification PII in bulk.
4854
5031
  // It is administrator-only and audit-logged server-side (403 for ordinary agents); the
@@ -5725,6 +5902,42 @@ async function handleTool(name, args) {
5725
5902
  if (args.teamId != null) qs.set('teamId', String(args.teamId));
5726
5903
  return await messagingGet(`/Reports/chat-intelligence/themes?${qs.toString()}`);
5727
5904
  }
5905
+ case 'chat_topics_get':
5906
+ return await messagingGet('/ChatIntelligenceTopics');
5907
+ case 'chat_topics_set':
5908
+ return args.industry
5909
+ ? await messagingPost('/ChatIntelligenceTopics/template', { industry: args.industry })
5910
+ : await messagingPut('/ChatIntelligenceTopics', { topics: args.topics ?? [] });
5911
+ case 'chat_topics_edit':
5912
+ return await messagingPost('/ChatIntelligenceTopics/mapping', { ops: [{ op: args.op, from: args.from ?? [], to: args.to ?? null, group: args.group ?? null }] });
5913
+ case 'chat_topics_group_suggestions':
5914
+ return await messagingGet(`/ChatIntelligenceTopics/group-suggestions?days=${args.days ?? 90}`);
5915
+ case 'chat_topics_suggestions':
5916
+ return await messagingGet(`/ChatIntelligenceTopics/suggestions?days=${args.days ?? 90}`);
5917
+ case 'chat_topics_resolve_suggestion':
5918
+ return await messagingPost('/ChatIntelligenceTopics/suggestions/resolve', { suggestion: args.suggestion, action: args.action, target: args.target ?? null });
5919
+ case 'chat_topics_reclassify':
5920
+ return args.confirm
5921
+ ? await messagingPost('/ChatIntelligenceTopics/reclassify', { scope: args.scope ?? 'other', days: args.days ?? 90, max: args.max ?? null, confirm: true })
5922
+ : await messagingGet(`/ChatIntelligenceTopics/reclassify/estimate?scope=${args.scope ?? 'other'}&days=${args.days ?? 90}${args.max ? `&max=${args.max}` : ''}`);
5923
+ case 'chat_topics_analysis':
5924
+ if (args.proposalId) return await messagingPost('/ChatIntelligenceTopics/analysis/proposals', { id: args.proposalId, accept: args.accept !== false });
5925
+ if (args.confirm) return await messagingPost('/ChatIntelligenceTopics/analysis', { confirm: true });
5926
+ return { estimate: await messagingGet('/ChatIntelligenceTopics/analysis/estimate'), cached: await messagingGet('/ChatIntelligenceTopics/analysis') };
5927
+ case 'canned_learning_suggestions':
5928
+ return await messagingGet(`/CannedLearning/suggestions?state=${encodeURIComponent(args.state ?? 'pending_review')}`);
5929
+ case 'canned_learning_generate':
5930
+ return await messagingPost('/CannedLearning/generate', {});
5931
+ case 'canned_learning_approve':
5932
+ return await messagingPost(`/CannedLearning/suggestions/${args.suggestionId}/approve`, { editedText: args.editedText ?? null });
5933
+ case 'canned_learning_reject':
5934
+ return await messagingPost(`/CannedLearning/suggestions/${args.suggestionId}/reject`, {});
5935
+ case 'canned_learning_edit_summary':
5936
+ return await messagingGet('/CannedLearning/edit-events');
5937
+ case 'canned_learning_record_edit':
5938
+ return await messagingPost('/CannedLearning/edit-events', { cannedResponseId: args.cannedResponseId, finalText: args.finalText });
5939
+ case 'canned_learning_rollback':
5940
+ return await messagingPost(`/CannedLearning/suggestions/${args.suggestionId}/rollback`, { notes: args.notes ?? null });
5728
5941
  case 'chat_intelligence_recommendations':
5729
5942
  return await messagingGet(`/Reports/chat-intelligence/recommendations`);
5730
5943
  case 'chat_intelligence_draft_article':
@@ -5973,7 +6186,7 @@ async function handleTool(name, args) {
5973
6186
  return await rpc('ai_config_set_reply_format', args);
5974
6187
 
5975
6188
  case 'site_identity_secret_status': {
5976
- const settings = await api('GET', '/SiteSettings');
6189
+ const settings = await api('GET', '/api/SiteSettings');
5977
6190
  const configured = settings?.identitySigningSecretConfigured ?? false;
5978
6191
  const masked = settings?.identitySigningSecretMasked ?? null;
5979
6192
  if (!configured) {
@@ -6013,7 +6226,7 @@ async function handleTool(name, args) {
6013
6226
  }
6014
6227
 
6015
6228
  case 'index_list': {
6016
- const indexes = await messagingApi('/AzureIndexes/GetIndexList');
6229
+ const indexes = await messagingApi('/AzureIndexes/List');
6017
6230
  if (!Array.isArray(indexes) || !indexes.length) {
6018
6231
  return 'No knowledge collections found for this site.';
6019
6232
  }
@@ -6345,7 +6558,8 @@ async function handleTool(name, args) {
6345
6558
  }
6346
6559
 
6347
6560
  case 'routing_delete': {
6348
- await messagingRequest('DELETE', `/rules/${args.id}`);
6561
+ // RulesController.DeleteAsync takes the rule in the BODY (DELETE /Rules {id}); DELETE /rules/{id} never existed.
6562
+ await messagingRequest('DELETE', '/Rules', { id: args.id });
6349
6563
  return `Deleted routing rule id=${args.id}`;
6350
6564
  }
6351
6565
 
@@ -6884,7 +7098,7 @@ async function handleTool(name, args) {
6884
7098
 
6885
7099
  // ── Layer 7: Admin Activity Log — recent channel/auth/integration events
6886
7100
  // Useful for detecting send failures, auth events, channel errors not in integration logs.
6887
- const activityLogP = safeJson(() => api('GET', '/AdminActivityLog?pageSize=10'));
7101
+ const activityLogP = safeJson(() => messagingRequest('GET', '/AdminActivityLog?pageSize=10'));
6888
7102
 
6889
7103
  const [health, ci, aiEx, velaroLogs, loggly, intLogPage, skillErr, activityLog] = await Promise.all([healthP, ciP, aiExP, velaroLoggerP, logglyP, intLogP, skillErrP, activityLogP]);
6890
7104
 
@@ -7009,7 +7223,7 @@ async function handleTool(name, args) {
7009
7223
  const q = args.query ?? '';
7010
7224
  const top = args.top ?? 5;
7011
7225
  if (!q) return 'query is required.';
7012
- const results = await api('GET', `/AzureIndexes/content-search?q=${encodeURIComponent(q)}&top=${top}`);
7226
+ const results = await messagingRequest('GET', `/AzureIndexes/content-search?q=${encodeURIComponent(q)}&top=${top}`);
7013
7227
  if (results?.error) return `Index search error: ${results.error}`;
7014
7228
  if (!Array.isArray(results) || !results.length) return `No results for "${q}". Index may be empty — trigger ingestion first.`;
7015
7229
  const lines = [
@@ -7072,7 +7286,7 @@ async function handleTool(name, args) {
7072
7286
  return JSON.stringify(r, null, 2);
7073
7287
  }
7074
7288
  case 'index_status': {
7075
- const sources = await api('GET', '/AzureIndexes/IntegrationSources');
7289
+ const sources = await messagingRequest('GET', '/AzureIndexes/IntegrationSources');
7076
7290
  if (!Array.isArray(sources) || !sources.length) return 'No indexed sources found. Run index_ingest to populate.';
7077
7291
  const lines = [
7078
7292
  '=== Index Status ===',
@@ -7085,7 +7299,7 @@ async function handleTool(name, args) {
7085
7299
  if (!args.jobId) return 'jobId is required.';
7086
7300
  const body = { jobId: args.jobId };
7087
7301
  if (args.indexName) body.indexName = args.indexName;
7088
- const result = await api('POST', '/AzureIndexes/IngestJobDirect', body);
7302
+ const result = await messagingRequest('POST', '/AzureIndexes/IngestJobDirect', body);
7089
7303
  if (result?.error) return `Ingestion error: ${result.error}`;
7090
7304
  return `Ingestion started for job ${args.jobId}.\n Physical index: ${result?.physicalIndex ?? 'unknown'}\n\nEmbedding runs in the background (~1-2 min). Use index_status to verify chunks appear.`;
7091
7305
  }
@@ -7110,7 +7324,7 @@ async function handleTool(name, args) {
7110
7324
  }
7111
7325
 
7112
7326
  case 'uploaded_files_list': {
7113
- const files = await api('GET', '/uploadedFiles/list/AI');
7327
+ const files = await messagingRequest('GET', '/uploadedFiles/list/AI');
7114
7328
  if (!Array.isArray(files) || !files.length) return 'No AI training files uploaded for this site.';
7115
7329
  const lines = ['=== Uploaded AI Training Files ===',
7116
7330
  ...files.map(f => ` [id=${f.id}] ${f.fileName} size=${f.fileLength ? Math.round(f.fileLength/1024)+'KB' : '?'}`)];
@@ -7123,7 +7337,7 @@ async function handleTool(name, args) {
7123
7337
  if (args.fileId) body.id = args.fileId;
7124
7338
  if (args.fileName) body.fileName = args.fileName;
7125
7339
  if (args.indexName) body.indexName = args.indexName;
7126
- const result = await api('POST', '/uploadedFiles/reindex', body);
7340
+ const result = await messagingRequest('POST', '/uploadedFiles/reindex', body);
7127
7341
  if (result?.error) return `Reindex error: ${result.error}`;
7128
7342
  return `✅ Re-indexed '${result.fileName}': ${result.chunksIndexed} chunks indexed.\n\nUse index_status to verify the chunks appear in the knowledge index.`;
7129
7343
  }
@@ -8266,6 +8480,23 @@ async function handleTool(name, args) {
8266
8480
  return `Subscribed to ${args.reportType} (${args.frequency ?? 'weekly'}, ${args.deliveryHour ?? 7}:00 site time).`;
8267
8481
  }
8268
8482
 
8483
+ case 'enterprise_reports': {
8484
+ const ER_PATHS = {
8485
+ program_breakdown: 'program-breakdown', agent_scorecard: 'agent-scorecard', qa_coverage: 'qa-coverage',
8486
+ coaching_queue: 'coaching-queue', sentiment_breakdown: 'sentiment-breakdown', topic_radar: 'topic-radar',
8487
+ weekly_brief: 'weekly-brief', weekly_brief_send_test: 'weekly-brief/send-test',
8488
+ };
8489
+ const seg = ER_PATHS[args.report];
8490
+ if (!seg) throw new Error('report must be one of: ' + Object.keys(ER_PATHS).join(', '));
8491
+ if (args.report === 'weekly_brief_send_test') return JSON.stringify(await messagingPost('/api/enterprise-reports/' + seg, {}), null, 2);
8492
+ const qs = new URLSearchParams();
8493
+ for (const k of ['from', 'to', 'groupBy', 'deploymentId', 'teamId', 'threshold', 'perAgent', 'limit', 'days']) {
8494
+ if (args[k] != null && args[k] !== '') qs.set(k, String(args[k]));
8495
+ }
8496
+ const q = qs.toString();
8497
+ return JSON.stringify(await messagingGet('/api/enterprise-reports/' + seg + (q ? '?' + q : '')), null, 2);
8498
+ }
8499
+
8269
8500
  case 'experience_impact_report': {
8270
8501
  const qs = new URLSearchParams();
8271
8502
  for (const k of ['from', 'to', 'teamId']) if (args[k] != null && args[k] !== '') qs.set(k, String(args[k]));
@@ -8394,6 +8625,9 @@ async function handleTool(name, args) {
8394
8625
  ` Enabled: ${data.enabled}`,
8395
8626
  ` "Call me": ${data.showCallMeOption}`,
8396
8627
  ` Callback window: ${data.callbackConfirmWindowMinutes} min`,
8628
+ ` Display name: ${data.displayName || '(AI)'}`,
8629
+ ` Badge shown: ${data.showBadge}`,
8630
+ ` Size: ${data.sizePx}px`,
8397
8631
  ].join('\n');
8398
8632
  }
8399
8633
 
@@ -8405,6 +8639,10 @@ async function handleTool(name, args) {
8405
8639
  if (args.enabled !== undefined) body.enabled = args.enabled;
8406
8640
  if (args.showCallMeOption !== undefined) body.showCallMeOption = args.showCallMeOption;
8407
8641
  if (args.callbackConfirmWindowMinutes !== undefined) body.callbackConfirmWindowMinutes = args.callbackConfirmWindowMinutes;
8642
+ if (args.displayName !== undefined) body.displayName = args.displayName;
8643
+ if (args.showBadge !== undefined) body.showBadge = args.showBadge;
8644
+ if (args.sizePx !== undefined) body.sizePx = args.sizePx;
8645
+ if (args.allowVisitorCollapse !== undefined) body.allowVisitorCollapse = args.allowVisitorCollapse;
8408
8646
  for (const [arg, max] of [['maxAvatarMinutesPerMonth', 100000], ['maxAvatarTtsChars', 100000000]]) {
8409
8647
  if (args[arg] === undefined) continue;
8410
8648
  if (!Number.isInteger(args[arg]) || args[arg] < 1 || args[arg] > max) throw new Error(`${arg} must be a whole number between 1 and ${max} (never unlimited).`);
@@ -8524,6 +8762,23 @@ async function handleTool(name, args) {
8524
8762
  return JSON.stringify(data, null, 2);
8525
8763
  }
8526
8764
 
8765
+ case 'email_inbox_resubscribe_outlook': {
8766
+ if (!args.siteId || !args.mailboxId) return 'siteId and mailboxId are required.';
8767
+ const data = await messagingPost(`/SupportTools/sites/${args.siteId}/email-inbox/outlook/${args.mailboxId}/resubscribe`, {});
8768
+ return JSON.stringify(data, null, 2);
8769
+ }
8770
+
8771
+ case 'site_integration_logs': {
8772
+ if (!args.siteId) return 'siteId is required.';
8773
+ const q = new URLSearchParams();
8774
+ if (args.slug) q.set('slug', args.slug);
8775
+ if (args.severity) q.set('severity', args.severity);
8776
+ if (args.search) q.set('search', args.search);
8777
+ if (args.take) q.set('take', String(args.take));
8778
+ const data = await messagingGet(`/SupportTools/sites/${args.siteId}/integration-logs?${q.toString()}`);
8779
+ return JSON.stringify(data, null, 2);
8780
+ }
8781
+
8527
8782
  case 'email_inbox_set_team': {
8528
8783
  if (!args.siteId || !args.provider || !args.mailboxId || args.teamId == null) return 'siteId, provider, mailboxId, and teamId are required.';
8529
8784
  const data = await messagingPatch(`/SupportTools/sites/${args.siteId}/email-inbox/${args.provider}/${args.mailboxId}/team`, { teamId: args.teamId });
@@ -8989,7 +9244,7 @@ async function handleTool(name, args) {
8989
9244
 
8990
9245
  case 'magento_diagnose': {
8991
9246
  if (!args.siteId) return 'siteId is required.';
8992
- const r = await api('GET', `/diagnostics/test-magento?siteId=${encodeURIComponent(args.siteId)}`);
9247
+ const r = await messagingRequest('GET', `/diagnostics/test-magento?siteId=${encodeURIComponent(args.siteId)}`);
8993
9248
  return JSON.stringify(r, null, 2);
8994
9249
  }
8995
9250
 
@@ -8997,18 +9252,18 @@ async function handleTool(name, args) {
8997
9252
  case 'sap_diagnose': {
8998
9253
  if (!args.siteId) return 'siteId is required.';
8999
9254
  const deep = args.deep === false ? 'false' : 'true';
9000
- const r = await api('GET', `/diagnostics/test-sap?siteId=${encodeURIComponent(args.siteId)}&deep=${deep}`);
9255
+ const r = await messagingRequest('GET', `/diagnostics/test-sap?siteId=${encodeURIComponent(args.siteId)}&deep=${deep}`);
9001
9256
  return JSON.stringify(r, null, 2);
9002
9257
  }
9003
9258
 
9004
9259
  case 'business_action_list': {
9005
- const r = await api('GET', '/BusinessActions');
9260
+ const r = await messagingRequest('GET', '/BusinessActions');
9006
9261
  return JSON.stringify(r, null, 2);
9007
9262
  }
9008
9263
 
9009
9264
  case 'business_action_route': {
9010
9265
  if (!args.text) return 'text is required.';
9011
- const r = await api('POST', '/BusinessActions/Route', { text: args.text, max: args.max });
9266
+ const r = await messagingRequest('POST', '/BusinessActions/Route', { text: args.text, max: args.max });
9012
9267
  return JSON.stringify(r, null, 2);
9013
9268
  }
9014
9269
 
@@ -9018,7 +9273,7 @@ async function handleTool(name, args) {
9018
9273
  const qs = [];
9019
9274
  if (args.idempotencyKey) qs.push(`idempotencyKey=${encodeURIComponent(args.idempotencyKey)}`);
9020
9275
  if (args.conversationId) qs.push(`conversationId=${encodeURIComponent(args.conversationId)}`);
9021
- const r = await api('POST', `/BusinessActions/${encodeURIComponent(args.name)}/Execute${qs.length ? '?' + qs.join('&') : ''}`, args.request);
9276
+ const r = await messagingRequest('POST', `/BusinessActions/${encodeURIComponent(args.name)}/Execute${qs.length ? '?' + qs.join('&') : ''}`, args.request);
9022
9277
  return JSON.stringify(r, null, 2);
9023
9278
  }
9024
9279
 
@@ -9561,7 +9816,7 @@ async function handleTool(name, args) {
9561
9816
  }
9562
9817
 
9563
9818
  case 'billing_plan_status': {
9564
- const s = await api('GET', '/BillingPayment/plan-status', undefined, MESSAGING_API);
9819
+ const s = await api('GET', '/api/BillingPayment/plan-status');
9565
9820
  const lines = [`Site ${s.siteId} — Subscription Status`];
9566
9821
  if (s.subscription) {
9567
9822
  lines.push(`Seats: ${s.subscription.maxLicensedSeats} | Conv/mo: ${s.subscription.maxConversationsPerMonth || '∞'} | Self-serve: ${s.subscription.enableSelfServeSeats ? 'YES' : 'no'}`);
@@ -9678,6 +9933,18 @@ async function handleTool(name, args) {
9678
9933
  return lines.join('\n');
9679
9934
  }
9680
9935
 
9936
+ case 'entitlement_enterprise_reports_package': {
9937
+ const q = args && args.siteId ? `?siteId=${encodeURIComponent(args.siteId)}` : '';
9938
+ const pkg = await api('GET', `/Entitlements/packages/enterprise-reports${q}`);
9939
+ const lines = [`${pkg.displayName} (${pkg.packageKey}) - plan ${pkg.plan}, default OFF${pkg.siteId ? `, site ${pkg.siteId}` : ''}`, ''];
9940
+ for (const it of pkg.items) {
9941
+ lines.push(`${pkg.siteId ? (it.enabled ? '[ON ] ' : '[off] ') : ''}${it.ask}. ${it.title}`);
9942
+ lines.push(` key: ${it.featureKey}; builds on: ${it.buildsOn.join(', ')}`);
9943
+ lines.push(` ${it.summary}`);
9944
+ }
9945
+ return lines.join(String.fromCharCode(10));
9946
+ }
9947
+
9681
9948
  case 'entitlement_list_features': {
9682
9949
  const features = await api('GET', '/Entitlements/features');
9683
9950
  const byCategory = {};
@@ -10045,6 +10312,61 @@ async function handleTool(name, args) {
10045
10312
  return JSON.stringify(listing, null, 2);
10046
10313
  }
10047
10314
 
10315
+ case 'recruiting_ats_failures': {
10316
+ const rows = await messagingGet('/Recruiting/Applications/AtsFailures');
10317
+ return JSON.stringify(rows, null, 2);
10318
+ }
10319
+ case 'recruiting_ats_retry': {
10320
+ const id = Number(args.applicationId);
10321
+ if (!Number.isInteger(id) || id <= 0) throw new Error('applicationId must be a positive integer.');
10322
+ const r = await messagingPost(`/Recruiting/Applications/${id}/AtsRetry`, {});
10323
+ return JSON.stringify(r, null, 2);
10324
+ }
10325
+ case 'recruiting_apply_settings': {
10326
+ const body = {};
10327
+ if (args.publicApplyEnabled !== undefined) body.publicApplyEnabled = Boolean(args.publicApplyEnabled);
10328
+ if (args.autoCreateCaseTypeId !== undefined) {
10329
+ const n = Number(args.autoCreateCaseTypeId);
10330
+ if (!Number.isInteger(n) || n < 0) throw new Error('autoCreateCaseTypeId must be an integer >= 0 (0 clears).');
10331
+ body.autoCreateCaseTypeId = n;
10332
+ }
10333
+ if (!Object.keys(body).length) throw new Error('Specify publicApplyEnabled and/or autoCreateCaseTypeId.');
10334
+ const r = await messagingPut('/Recruiting/ApplySettings', body);
10335
+ return JSON.stringify(r, null, 2);
10336
+ }
10337
+ case 'recruiting_bot_settings': {
10338
+ const fields = ['pageContextEnabled', 'revealPostedDate', 'revealDescription', 'revealApplyLink', 'publicApplyEnabled', 'applyMode', 'maxResults', 'greeting', 'tone'];
10339
+ const body = {};
10340
+ for (const f of fields) if (args[f] !== undefined) body[f] = args[f];
10341
+ if (body.applyMode !== undefined && !['both', 'link', 'inproduct'].includes(String(body.applyMode).toLowerCase())) throw new Error('applyMode must be both, link or inproduct.');
10342
+ if (body.tone !== undefined && !['friendly', 'professional', 'concise'].includes(String(body.tone).toLowerCase())) throw new Error('tone must be friendly, professional or concise.');
10343
+ if (body.maxResults !== undefined && !(Number.isInteger(Number(body.maxResults)) && Number(body.maxResults) >= 1 && Number(body.maxResults) <= 10)) throw new Error('maxResults must be an integer from 1 to 10.');
10344
+ const r = Object.keys(body).length ? await messagingPut('/Recruiting/BotSettings', body) : await messagingGet('/Recruiting/BotSettings');
10345
+ return JSON.stringify(r, null, 2);
10346
+ }
10347
+ case 'recruiting_download_resume': {
10348
+ const id = Number(args.applicationId);
10349
+ if (!Number.isInteger(id) || id <= 0) throw new Error('applicationId must be a positive integer.');
10350
+ if (!MESSAGING_API) throw new Error('VELARO_MESSAGING_API is not configured.');
10351
+ const res = await fetch(`${MESSAGING_API}/Recruiting/Applications/${id}/Resume`, {
10352
+ headers: { Authorization: authHeader() },
10353
+ signal: AbortSignal.timeout(30000),
10354
+ });
10355
+ if (!res.ok) {
10356
+ const text = await res.text().catch(() => '');
10357
+ throw new Error(`/Recruiting/Applications/${id}/Resume -> ${res.status}: ${text.slice(0, 300)}`);
10358
+ }
10359
+ const buf = Buffer.from(await res.arrayBuffer());
10360
+ if (buf.length > 5 * 1024 * 1024) throw new Error(`Resume is ${buf.length} bytes (over the 5 MB MCP limit); use the CLI: velaro recruiting applications resume ${id}`);
10361
+ const cd = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(res.headers.get('content-disposition') || '');
10362
+ return JSON.stringify({
10363
+ fileName: cd ? decodeURIComponent(cd[1]) : `resume-${id}`,
10364
+ contentType: res.headers.get('content-type') || 'application/octet-stream',
10365
+ sizeBytes: buf.length,
10366
+ base64: buf.toString('base64'),
10367
+ }, null, 2);
10368
+ }
10369
+
10048
10370
  case 'jobboard_move_application_stage': {
10049
10371
  const app = await messagingPut(`/Recruiting/Applications/${args.applicationId}/Stage`, { stage: args.stage });
10050
10372
  return JSON.stringify(app, null, 2);