@velaro/mcp-server 0.6.91 → 0.6.93

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 +2 -2
  2. package/server.js +328 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "0.6.91",
3
+ "version": "0.6.93",
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": {
@@ -10,7 +10,7 @@
10
10
  "node": ">=18.0.0"
11
11
  },
12
12
  "dependencies": {
13
- "@modelcontextprotocol/sdk": "^1.30.0",
13
+ "@modelcontextprotocol/sdk": "^1.32.1",
14
14
  "qs": "^6.16.0"
15
15
  },
16
16
  "overrides": {
package/server.js CHANGED
@@ -857,6 +857,46 @@ const TOOLS = [
857
857
  description: 'Roll back an approved suggestion, restoring the original canned message text.',
858
858
  inputSchema: { type: 'object', properties: { suggestionId: { type: 'number' }, notes: { type: 'string' } }, required: ['suggestionId'] },
859
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
+ },
860
900
  {
861
901
  name: 'chat_intelligence_recommendations',
862
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).',
@@ -1008,6 +1048,7 @@ Key node shapes:
1008
1048
  propName:"myAnswerVar" } // variable that stores the result as JSON array string
1009
1049
  • condition: { type:"action", actionType:"condition", conditionOptions:[...] }
1010
1050
  • transferToAI: { type:"action", actionType:"transferToAI", aiConfigId:N }
1051
+ 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.
1011
1052
 
1012
1053
  For multiSelect dynamic options: the variable named in dynamicOptionsVariable must contain a JSON array like [{"label":"Red","value":"red"},{"label":"Blue","value":"blue"}]. Populate it via an HTTP Request node before the ask node.`,
1013
1054
  inputSchema: {
@@ -1021,6 +1062,20 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1021
1062
  },
1022
1063
  },
1023
1064
 
1065
+ {
1066
+ name: 'ai_followups_site_get',
1067
+ description: "Read this account's default for AI follow-up suggestions (the AI Chatbot node's \"AI mode\": after each web-chat bot reply the bot offers 2-4 short tappable follow-up replies). A node's own suggestFollowUps setting wins over this default; platform default is off.",
1068
+ inputSchema: { type: 'object', properties: {}, required: [] },
1069
+ },
1070
+ {
1071
+ name: 'ai_followups_site_set',
1072
+ description: "Set this account's default for AI follow-up suggestions. enabled=true/false, or omit/null to clear (platform default: off). Per-node settings (workflow_update_node: suggestFollowUps) override this. Returns the value read back from the server.",
1073
+ inputSchema: {
1074
+ type: 'object',
1075
+ properties: { enabled: { type: ['boolean', 'null'], description: 'true = on, false = off, null/omitted = clear' } },
1076
+ required: [],
1077
+ },
1078
+ },
1024
1079
  // ── Surveys ──────────────────────────────────────────────────────────────
1025
1080
  {
1026
1081
  name: 'survey_list',
@@ -1554,10 +1609,28 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1554
1609
  },
1555
1610
  buttonLabel: { type: 'string', description: 'Optional action button label' },
1556
1611
  buttonUrl: { type: 'string', description: 'Optional action button URL (required if buttonLabel is set)' },
1612
+ 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'] } },
1613
+ 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.' },
1557
1614
  },
1558
1615
  required: ['title'],
1559
1616
  },
1560
1617
  },
1618
+ {
1619
+ name: 'teams_send_direct',
1620
+ description: 'Send a 1:1 Teams message (Adaptive Card) directly to any user in the Velaro tenant by UPN/email or Entra object id. Velaro staff only. Graph chat first, Velaro Bot proactive fallback.',
1621
+ inputSchema: {
1622
+ type: 'object',
1623
+ properties: {
1624
+ user: { type: 'string', description: 'UPN/email (alex@velaro.com) or Entra object id' },
1625
+ title: { type: 'string', description: 'Card title' },
1626
+ message: { type: 'string', description: 'Message text' },
1627
+ fields: { type: 'array', description: 'Optional fact rows', items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } }, required: ['label', 'value'] } },
1628
+ buttonLabel: { type: 'string', description: 'Optional button label' },
1629
+ buttonUrl: { type: 'string', description: 'Optional button URL' },
1630
+ },
1631
+ required: ['user', 'title'],
1632
+ },
1633
+ },
1561
1634
 
1562
1635
  // ── Slack ─────────────────────────────────────────────────────────────────
1563
1636
  {
@@ -3017,7 +3090,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3017
3090
  inputSchema: {
3018
3091
  type: 'object',
3019
3092
  properties: {
3020
- report: { type: 'string', enum: ['program_breakdown', 'agent_scorecard', 'qa_coverage', 'coaching_queue', 'sentiment_breakdown', 'topic_radar', 'weekly_brief', 'weekly_brief_send_test'] },
3093
+ report: { type: 'string', enum: ['program_breakdown', 'agent_scorecard', 'qa_coverage', 'coaching_queue', 'sentiment_breakdown', 'topic_radar', 'weekly_brief', 'weekly_brief_send_test', 'repeat_contact', 'demand_coverage', 'bot_containment', 'effort_index', 'promise_ledger', 'promise_dismiss'] },
3021
3094
  from: { type: 'string', description: 'yyyy-MM-dd start (default: 30 days ago).' },
3022
3095
  to: { type: 'string', description: 'yyyy-MM-dd end, inclusive (default: today).' },
3023
3096
  groupBy: { type: 'string', description: 'program_breakdown: website|team|platform|device|entrypage. sentiment_breakdown: website|team|agent|theme.' },
@@ -3027,6 +3100,14 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3027
3100
  perAgent: { type: 'number', description: 'coaching_queue: chats listed per agent (default 5).' },
3028
3101
  limit: { type: 'number', description: 'topic_radar: max topics (default 25).' },
3029
3102
  days: { type: 'number', description: 'weekly_brief: days to cover (default 7).' },
3103
+ windowDays: { type: 'number', description: 'repeat_contact: days a contact has to come back to count as a repeat (1-30, default 7).' },
3104
+ weeks: { type: 'number', description: 'demand_coverage: whole local weeks of history (1-12, default 6). from/to are IGNORED by demand_coverage.' },
3105
+ handleMinutes: { type: 'number', description: 'demand_coverage: assumed minutes per chat (1-120, default 10).' },
3106
+ maxConcurrent: { type: 'number', description: 'demand_coverage: assumed chats an agent handles at once (1-10, default 3).' },
3107
+ timeZone: { type: 'string', description: 'demand_coverage: IANA time zone (default America/Los_Angeles).' },
3108
+ lowScore: { type: 'number', description: 'bot_containment: QA score below which a handed-off chat counts as regret (1-100, default 60).' },
3109
+ includeDemo: { type: 'boolean', description: 'true = show only the seeded sample (demo) data. Default false.' },
3110
+ promiseId: { type: 'string', description: 'promise_dismiss: the promise id to dismiss (managers only).' },
3030
3111
  },
3031
3112
  required: ['report'],
3032
3113
  },
@@ -3214,6 +3295,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3214
3295
  displayName: { type: 'string', description: 'Visitor-facing avatar name shown on the badge (e.g. "Moshky"). Empty string resets to "AI".' },
3215
3296
  showBadge: { type: 'boolean', description: 'Show the name badge over the avatar video. Default true.' },
3216
3297
  sizePx: { type: 'number', description: 'Max avatar panel width in px (160-640, square). Default 360.' },
3298
+ allowVisitorCollapse: { type: 'boolean', description: 'Let visitors hide or collapse the avatar panel (up/down arrow and X). Default true.' },
3217
3299
  maxAvatarMinutesPerMonth: { type: 'number', description: 'Monthly avatar video minutes cap for the site. Whole number 1-100000; never unlimited (0/negative rejected).' },
3218
3300
  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.' },
3219
3301
  },
@@ -3709,6 +3791,15 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3709
3791
  required: [],
3710
3792
  },
3711
3793
  },
3794
+ {
3795
+ name: 'ai_federation',
3796
+ description: 'SuperAdmin: Anthropic Workload Identity Federation for the AI gateway. action=status shows enabled, circuit breaker state, last success/failure reason, federated vs API-key fallback counts and token expiry. action=test does a real token exchange plus a 1-token Haiku message and reports which path (federated, apikey, apikey-fallback) served it. action=reset_breaker closes the breaker so the next call retries federation.',
3797
+ inputSchema: {
3798
+ type: 'object',
3799
+ properties: { action: { type: 'string', enum: ['status', 'test', 'reset_breaker'], description: 'Default status.' } },
3800
+ required: [],
3801
+ },
3802
+ },
3712
3803
  {
3713
3804
  name: 'status_incident_list',
3714
3805
  description: 'Velaro staff only (SuperAdmin). Lists public status page incidents, including component overrides (IncidentType "Override").',
@@ -4939,6 +5030,53 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
4939
5030
  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.',
4940
5031
  inputSchema: { type: 'object', properties: {}, required: [] },
4941
5032
  },
5033
+ {
5034
+ name: 'recruiting_ats_failures',
5035
+ description: 'List candidate applications whose ATS write-back is Pending, Failed or Abandoned (status, attempts, last error, next retry). Requires EnableRecruitingBot.',
5036
+ inputSchema: { type: 'object', properties: {}, required: [] },
5037
+ },
5038
+ {
5039
+ name: 'recruiting_ats_retry',
5040
+ description: 'Retry the ATS write-back for one candidate application right now (resets the attempt counter). Returns the new sync status.',
5041
+ inputSchema: { type: 'object', properties: { applicationId: { type: 'number' } }, required: ['applicationId'] },
5042
+ },
5043
+ {
5044
+ name: 'recruiting_apply_settings',
5045
+ 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).',
5046
+ inputSchema: {
5047
+ type: 'object',
5048
+ properties: {
5049
+ publicApplyEnabled: { type: 'boolean' },
5050
+ autoCreateCaseTypeId: { type: 'number', description: 'Case type id; 0 clears the auto-create link' },
5051
+ defaultListingDays: { type: 'number', description: '1-365: how many days a new listing stays live when its feed gives no expiry. Requires the Job Board lifecycle package.' },
5052
+ },
5053
+ required: [],
5054
+ },
5055
+ },
5056
+ {
5057
+ name: 'recruiting_bot_settings',
5058
+ 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).',
5059
+ inputSchema: {
5060
+ type: 'object',
5061
+ properties: {
5062
+ pageContextEnabled: { type: 'boolean' },
5063
+ revealPostedDate: { type: 'boolean' },
5064
+ revealDescription: { type: 'boolean' },
5065
+ revealApplyLink: { type: 'boolean' },
5066
+ publicApplyEnabled: { type: 'boolean' },
5067
+ applyMode: { type: 'string', enum: ['both', 'link', 'inproduct'] },
5068
+ maxResults: { type: 'number' },
5069
+ greeting: { type: 'string' },
5070
+ tone: { type: 'string', enum: ['friendly', 'professional', 'concise'] },
5071
+ },
5072
+ required: [],
5073
+ },
5074
+ },
5075
+ {
5076
+ name: 'recruiting_download_resume',
5077
+ 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.',
5078
+ inputSchema: { type: 'object', properties: { applicationId: { type: 'number' } }, required: ['applicationId'] },
5079
+ },
4942
5080
  // Recruiting/Applications/ExportEeoc (voluntary self-ID CSV export) is intentionally NOT
4943
5081
  // exposed as an MCP tool -- it returns sensitive EEOC/OFCCP self-identification PII in bulk.
4944
5082
  // It is administrator-only and audit-logged server-side (403 for ordinary agents); the
@@ -5016,6 +5154,29 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
5016
5154
  required: ['sourceId', 'title'],
5017
5155
  },
5018
5156
  },
5157
+ // -- Job Board listing lifecycle (package "job-board-lifecycle", default OFF: 403 for a site without it) --
5158
+ {
5159
+ name: 'jobboard_lifecycle_status',
5160
+ description: 'Does this site have the Job Board listing lifecycle package (close/renew, expiry, JSON-LD listing pages), and what is its default listing duration? Returns { entitled, defaultListingDays }.',
5161
+ inputSchema: { type: 'object', properties: {}, required: [] },
5162
+ },
5163
+ {
5164
+ name: 'jobboard_close_listing',
5165
+ description: 'Close a job listing: it stops appearing in search and the public board, the public listing page answers 410, and applying to it answers 410. Idempotent. Requires the Job Board lifecycle package (403 otherwise); 404 for an id that is not on this site.',
5166
+ inputSchema: { type: 'object', properties: { listingId: { type: 'number' } }, required: ['listingId'] },
5167
+ },
5168
+ {
5169
+ name: 'jobboard_renew_listing',
5170
+ description: 'Renew a job listing: extends its expiry by `days` (default: the site\'s default listing days) from the later of its current expiry and now, increments its renewal count and republishes it if it was closed or expired. Requires the Job Board lifecycle package (403 otherwise).',
5171
+ inputSchema: {
5172
+ type: 'object',
5173
+ properties: {
5174
+ listingId: { type: 'number' },
5175
+ days: { type: 'number', description: '1-365; omit for the site default' },
5176
+ },
5177
+ required: ['listingId'],
5178
+ },
5179
+ },
5019
5180
  {
5020
5181
  name: 'jobboard_move_application_stage',
5021
5182
  description: 'Move a candidate application to a new HR/agency-side pipeline stage on the applicant Kanban board (Applied, Screened, Interview, Offer, Hired, Rejected).',
@@ -5815,6 +5976,28 @@ async function handleTool(name, args) {
5815
5976
  if (args.teamId != null) qs.set('teamId', String(args.teamId));
5816
5977
  return await messagingGet(`/Reports/chat-intelligence/themes?${qs.toString()}`);
5817
5978
  }
5979
+ case 'chat_topics_get':
5980
+ return await messagingGet('/ChatIntelligenceTopics');
5981
+ case 'chat_topics_set':
5982
+ return args.industry
5983
+ ? await messagingPost('/ChatIntelligenceTopics/template', { industry: args.industry })
5984
+ : await messagingPut('/ChatIntelligenceTopics', { topics: args.topics ?? [] });
5985
+ case 'chat_topics_edit':
5986
+ return await messagingPost('/ChatIntelligenceTopics/mapping', { ops: [{ op: args.op, from: args.from ?? [], to: args.to ?? null, group: args.group ?? null }] });
5987
+ case 'chat_topics_group_suggestions':
5988
+ return await messagingGet(`/ChatIntelligenceTopics/group-suggestions?days=${args.days ?? 90}`);
5989
+ case 'chat_topics_suggestions':
5990
+ return await messagingGet(`/ChatIntelligenceTopics/suggestions?days=${args.days ?? 90}`);
5991
+ case 'chat_topics_resolve_suggestion':
5992
+ return await messagingPost('/ChatIntelligenceTopics/suggestions/resolve', { suggestion: args.suggestion, action: args.action, target: args.target ?? null });
5993
+ case 'chat_topics_reclassify':
5994
+ return args.confirm
5995
+ ? await messagingPost('/ChatIntelligenceTopics/reclassify', { scope: args.scope ?? 'other', days: args.days ?? 90, max: args.max ?? null, confirm: true })
5996
+ : await messagingGet(`/ChatIntelligenceTopics/reclassify/estimate?scope=${args.scope ?? 'other'}&days=${args.days ?? 90}${args.max ? `&max=${args.max}` : ''}`);
5997
+ case 'chat_topics_analysis':
5998
+ if (args.proposalId) return await messagingPost('/ChatIntelligenceTopics/analysis/proposals', { id: args.proposalId, accept: args.accept !== false });
5999
+ if (args.confirm) return await messagingPost('/ChatIntelligenceTopics/analysis', { confirm: true });
6000
+ return { estimate: await messagingGet('/ChatIntelligenceTopics/analysis/estimate'), cached: await messagingGet('/ChatIntelligenceTopics/analysis') };
5818
6001
  case 'canned_learning_suggestions':
5819
6002
  return await messagingGet(`/CannedLearning/suggestions?state=${encodeURIComponent(args.state ?? 'pending_review')}`);
5820
6003
  case 'canned_learning_generate':
@@ -5885,6 +6068,17 @@ async function handleTool(name, args) {
5885
6068
  // [Authorize] does not accept vel_live_* service keys (only a handful of controllers opt
5886
6069
  // into that manually) — these tools need VELARO_JWT, same requirement as the existing
5887
6070
  // notifications_get/ai_config_staff_get tools they're modeled on.
6071
+ case 'ai_followups_site_get': {
6072
+ const r = await messagingGet('/SiteSettings/AiFollowUps');
6073
+ return JSON.stringify({ enabled: r?.enabled ?? null }, null, 2);
6074
+ }
6075
+
6076
+ case 'ai_followups_site_set': {
6077
+ await messagingPut('/SiteSettings/AiFollowUps', { enabled: typeof args.enabled === 'boolean' ? args.enabled : null });
6078
+ const r = await messagingGet('/SiteSettings/AiFollowUps');
6079
+ return JSON.stringify({ enabled: r?.enabled ?? null }, null, 2);
6080
+ }
6081
+
5888
6082
  case 'survey_list': {
5889
6083
  const surveys = (await messagingGet('/Surveys/list')) ?? [];
5890
6084
  if (!surveys.length) return 'No surveys found.';
@@ -6077,7 +6271,7 @@ async function handleTool(name, args) {
6077
6271
  return await rpc('ai_config_set_reply_format', args);
6078
6272
 
6079
6273
  case 'site_identity_secret_status': {
6080
- const settings = await api('GET', '/SiteSettings');
6274
+ const settings = await api('GET', '/api/SiteSettings');
6081
6275
  const configured = settings?.identitySigningSecretConfigured ?? false;
6082
6276
  const masked = settings?.identitySigningSecretMasked ?? null;
6083
6277
  if (!configured) {
@@ -6117,7 +6311,7 @@ async function handleTool(name, args) {
6117
6311
  }
6118
6312
 
6119
6313
  case 'index_list': {
6120
- const indexes = await messagingApi('/AzureIndexes/GetIndexList');
6314
+ const indexes = await messagingApi('/AzureIndexes/List');
6121
6315
  if (!Array.isArray(indexes) || !indexes.length) {
6122
6316
  return 'No knowledge collections found for this site.';
6123
6317
  }
@@ -6383,7 +6577,26 @@ async function handleTool(name, args) {
6383
6577
  return await api('PUT', '/TeamsBot/ChannelConfig', body);
6384
6578
  }
6385
6579
 
6580
+ case 'teams_send_direct': {
6581
+ const r = await api('POST', '/TeamsMessaging/Direct', {
6582
+ user: args.user, title: args.title, message: args.message || '',
6583
+ facts: (args.fields || []).map(f => ({ label: f.label, value: f.value })),
6584
+ actionUrl: args.buttonUrl, actionLabel: args.buttonLabel,
6585
+ });
6586
+ return r?.sent ? `Direct message sent to ${args.user} via ${r.path}.` : `Send failed: ${r?.error || 'unknown error'} ${(r?.warnings || []).join('; ')}`;
6587
+ }
6588
+
6386
6589
  case 'teams_notify_card': {
6590
+ if ((args.mentions && args.mentions.length) || args.destination) {
6591
+ const r = await api('POST', '/TeamsMessaging/Card', {
6592
+ title: args.title,
6593
+ facts: (args.fields || []).map(f => ({ label: f.label, value: f.value })),
6594
+ actionUrl: args.buttonUrl, actionLabel: args.buttonLabel,
6595
+ destination: args.destination,
6596
+ mentions: (args.mentions || []).map(m => ({ user: m.user, name: m.name })),
6597
+ });
6598
+ return r?.sent ? `Card sent via ${r.path}${r.warnings?.length ? ' (' + r.warnings.join('; ') + ')' : ''}.` : `Send failed: ${r?.error || 'unknown error'}`;
6599
+ }
6387
6600
  const webhooks = await api('GET', '/TeamsNotification');
6388
6601
  if (!Array.isArray(webhooks) || !webhooks.length)
6389
6602
  return 'No Teams webhooks configured. Go to Integrations → Microsoft Teams to add one, or call velaro teams create <name> <webhookUrl> via CLI.';
@@ -6449,7 +6662,8 @@ async function handleTool(name, args) {
6449
6662
  }
6450
6663
 
6451
6664
  case 'routing_delete': {
6452
- await messagingRequest('DELETE', `/rules/${args.id}`);
6665
+ // RulesController.DeleteAsync takes the rule in the BODY (DELETE /Rules {id}); DELETE /rules/{id} never existed.
6666
+ await messagingRequest('DELETE', '/Rules', { id: args.id });
6453
6667
  return `Deleted routing rule id=${args.id}`;
6454
6668
  }
6455
6669
 
@@ -6988,7 +7202,7 @@ async function handleTool(name, args) {
6988
7202
 
6989
7203
  // ── Layer 7: Admin Activity Log — recent channel/auth/integration events
6990
7204
  // Useful for detecting send failures, auth events, channel errors not in integration logs.
6991
- const activityLogP = safeJson(() => api('GET', '/AdminActivityLog?pageSize=10'));
7205
+ const activityLogP = safeJson(() => messagingRequest('GET', '/AdminActivityLog?pageSize=10'));
6992
7206
 
6993
7207
  const [health, ci, aiEx, velaroLogs, loggly, intLogPage, skillErr, activityLog] = await Promise.all([healthP, ciP, aiExP, velaroLoggerP, logglyP, intLogP, skillErrP, activityLogP]);
6994
7208
 
@@ -7113,7 +7327,7 @@ async function handleTool(name, args) {
7113
7327
  const q = args.query ?? '';
7114
7328
  const top = args.top ?? 5;
7115
7329
  if (!q) return 'query is required.';
7116
- const results = await api('GET', `/AzureIndexes/content-search?q=${encodeURIComponent(q)}&top=${top}`);
7330
+ const results = await messagingRequest('GET', `/AzureIndexes/content-search?q=${encodeURIComponent(q)}&top=${top}`);
7117
7331
  if (results?.error) return `Index search error: ${results.error}`;
7118
7332
  if (!Array.isArray(results) || !results.length) return `No results for "${q}". Index may be empty — trigger ingestion first.`;
7119
7333
  const lines = [
@@ -7176,7 +7390,7 @@ async function handleTool(name, args) {
7176
7390
  return JSON.stringify(r, null, 2);
7177
7391
  }
7178
7392
  case 'index_status': {
7179
- const sources = await api('GET', '/AzureIndexes/IntegrationSources');
7393
+ const sources = await messagingRequest('GET', '/AzureIndexes/IntegrationSources');
7180
7394
  if (!Array.isArray(sources) || !sources.length) return 'No indexed sources found. Run index_ingest to populate.';
7181
7395
  const lines = [
7182
7396
  '=== Index Status ===',
@@ -7189,7 +7403,7 @@ async function handleTool(name, args) {
7189
7403
  if (!args.jobId) return 'jobId is required.';
7190
7404
  const body = { jobId: args.jobId };
7191
7405
  if (args.indexName) body.indexName = args.indexName;
7192
- const result = await api('POST', '/AzureIndexes/IngestJobDirect', body);
7406
+ const result = await messagingRequest('POST', '/AzureIndexes/IngestJobDirect', body);
7193
7407
  if (result?.error) return `Ingestion error: ${result.error}`;
7194
7408
  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.`;
7195
7409
  }
@@ -7214,7 +7428,7 @@ async function handleTool(name, args) {
7214
7428
  }
7215
7429
 
7216
7430
  case 'uploaded_files_list': {
7217
- const files = await api('GET', '/uploadedFiles/list/AI');
7431
+ const files = await messagingRequest('GET', '/uploadedFiles/list/AI');
7218
7432
  if (!Array.isArray(files) || !files.length) return 'No AI training files uploaded for this site.';
7219
7433
  const lines = ['=== Uploaded AI Training Files ===',
7220
7434
  ...files.map(f => ` [id=${f.id}] ${f.fileName} size=${f.fileLength ? Math.round(f.fileLength/1024)+'KB' : '?'}`)];
@@ -7227,7 +7441,7 @@ async function handleTool(name, args) {
7227
7441
  if (args.fileId) body.id = args.fileId;
7228
7442
  if (args.fileName) body.fileName = args.fileName;
7229
7443
  if (args.indexName) body.indexName = args.indexName;
7230
- const result = await api('POST', '/uploadedFiles/reindex', body);
7444
+ const result = await messagingRequest('POST', '/uploadedFiles/reindex', body);
7231
7445
  if (result?.error) return `Reindex error: ${result.error}`;
7232
7446
  return `✅ Re-indexed '${result.fileName}': ${result.chunksIndexed} chunks indexed.\n\nUse index_status to verify the chunks appear in the knowledge index.`;
7233
7447
  }
@@ -8375,12 +8589,18 @@ async function handleTool(name, args) {
8375
8589
  program_breakdown: 'program-breakdown', agent_scorecard: 'agent-scorecard', qa_coverage: 'qa-coverage',
8376
8590
  coaching_queue: 'coaching-queue', sentiment_breakdown: 'sentiment-breakdown', topic_radar: 'topic-radar',
8377
8591
  weekly_brief: 'weekly-brief', weekly_brief_send_test: 'weekly-brief/send-test',
8592
+ repeat_contact: 'repeat-contact', demand_coverage: 'demand-coverage', bot_containment: 'bot-containment',
8593
+ effort_index: 'effort-index', promise_ledger: 'promise-ledger',
8378
8594
  };
8595
+ if (args.report === 'promise_dismiss') {
8596
+ if (!args.promiseId) throw new Error('promiseId is required for promise_dismiss');
8597
+ return JSON.stringify(await messagingPost('/api/enterprise-reports/promise-ledger/' + encodeURIComponent(String(args.promiseId)) + '/dismiss', {}), null, 2);
8598
+ }
8379
8599
  const seg = ER_PATHS[args.report];
8380
8600
  if (!seg) throw new Error('report must be one of: ' + Object.keys(ER_PATHS).join(', '));
8381
8601
  if (args.report === 'weekly_brief_send_test') return JSON.stringify(await messagingPost('/api/enterprise-reports/' + seg, {}), null, 2);
8382
8602
  const qs = new URLSearchParams();
8383
- for (const k of ['from', 'to', 'groupBy', 'deploymentId', 'teamId', 'threshold', 'perAgent', 'limit', 'days']) {
8603
+ for (const k of ['from', 'to', 'groupBy', 'deploymentId', 'teamId', 'threshold', 'perAgent', 'limit', 'days', 'windowDays', 'weeks', 'handleMinutes', 'maxConcurrent', 'timeZone', 'lowScore', 'includeDemo']) {
8384
8604
  if (args[k] != null && args[k] !== '') qs.set(k, String(args[k]));
8385
8605
  }
8386
8606
  const q = qs.toString();
@@ -8532,6 +8752,7 @@ async function handleTool(name, args) {
8532
8752
  if (args.displayName !== undefined) body.displayName = args.displayName;
8533
8753
  if (args.showBadge !== undefined) body.showBadge = args.showBadge;
8534
8754
  if (args.sizePx !== undefined) body.sizePx = args.sizePx;
8755
+ if (args.allowVisitorCollapse !== undefined) body.allowVisitorCollapse = args.allowVisitorCollapse;
8535
8756
  for (const [arg, max] of [['maxAvatarMinutesPerMonth', 100000], ['maxAvatarTtsChars', 100000000]]) {
8536
8757
  if (args[arg] === undefined) continue;
8537
8758
  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).`);
@@ -8797,6 +9018,13 @@ async function handleTool(name, args) {
8797
9018
  return JSON.stringify(data, null, 2);
8798
9019
  }
8799
9020
 
9021
+ case 'ai_federation': {
9022
+ const action = args.action || 'status';
9023
+ if (action === 'test') return JSON.stringify(await api('POST', '/AiFederation/test', {}), null, 2);
9024
+ if (action === 'reset_breaker') return JSON.stringify(await api('POST', '/AiFederation/reset-breaker', {}), null, 2);
9025
+ return JSON.stringify(await api('GET', '/AiFederation/status'), null, 2);
9026
+ }
9027
+
8800
9028
  case 'status_incident_list': {
8801
9029
  const rows = await messagingRequest('GET', `/superadmin/status-incidents?filter=${encodeURIComponent(args.filter || 'active')}`);
8802
9030
  return JSON.stringify(rows, null, 2);
@@ -9133,7 +9361,7 @@ async function handleTool(name, args) {
9133
9361
 
9134
9362
  case 'magento_diagnose': {
9135
9363
  if (!args.siteId) return 'siteId is required.';
9136
- const r = await api('GET', `/diagnostics/test-magento?siteId=${encodeURIComponent(args.siteId)}`);
9364
+ const r = await messagingRequest('GET', `/diagnostics/test-magento?siteId=${encodeURIComponent(args.siteId)}`);
9137
9365
  return JSON.stringify(r, null, 2);
9138
9366
  }
9139
9367
 
@@ -9141,18 +9369,18 @@ async function handleTool(name, args) {
9141
9369
  case 'sap_diagnose': {
9142
9370
  if (!args.siteId) return 'siteId is required.';
9143
9371
  const deep = args.deep === false ? 'false' : 'true';
9144
- const r = await api('GET', `/diagnostics/test-sap?siteId=${encodeURIComponent(args.siteId)}&deep=${deep}`);
9372
+ const r = await messagingRequest('GET', `/diagnostics/test-sap?siteId=${encodeURIComponent(args.siteId)}&deep=${deep}`);
9145
9373
  return JSON.stringify(r, null, 2);
9146
9374
  }
9147
9375
 
9148
9376
  case 'business_action_list': {
9149
- const r = await api('GET', '/BusinessActions');
9377
+ const r = await messagingRequest('GET', '/BusinessActions');
9150
9378
  return JSON.stringify(r, null, 2);
9151
9379
  }
9152
9380
 
9153
9381
  case 'business_action_route': {
9154
9382
  if (!args.text) return 'text is required.';
9155
- const r = await api('POST', '/BusinessActions/Route', { text: args.text, max: args.max });
9383
+ const r = await messagingRequest('POST', '/BusinessActions/Route', { text: args.text, max: args.max });
9156
9384
  return JSON.stringify(r, null, 2);
9157
9385
  }
9158
9386
 
@@ -9162,7 +9390,7 @@ async function handleTool(name, args) {
9162
9390
  const qs = [];
9163
9391
  if (args.idempotencyKey) qs.push(`idempotencyKey=${encodeURIComponent(args.idempotencyKey)}`);
9164
9392
  if (args.conversationId) qs.push(`conversationId=${encodeURIComponent(args.conversationId)}`);
9165
- const r = await api('POST', `/BusinessActions/${encodeURIComponent(args.name)}/Execute${qs.length ? '?' + qs.join('&') : ''}`, args.request);
9393
+ const r = await messagingRequest('POST', `/BusinessActions/${encodeURIComponent(args.name)}/Execute${qs.length ? '?' + qs.join('&') : ''}`, args.request);
9166
9394
  return JSON.stringify(r, null, 2);
9167
9395
  }
9168
9396
 
@@ -9705,7 +9933,7 @@ async function handleTool(name, args) {
9705
9933
  }
9706
9934
 
9707
9935
  case 'billing_plan_status': {
9708
- const s = await api('GET', '/BillingPayment/plan-status', undefined, MESSAGING_API);
9936
+ const s = await api('GET', '/api/BillingPayment/plan-status');
9709
9937
  const lines = [`Site ${s.siteId} — Subscription Status`];
9710
9938
  if (s.subscription) {
9711
9939
  lines.push(`Seats: ${s.subscription.maxLicensedSeats} | Conv/mo: ${s.subscription.maxConversationsPerMonth || '∞'} | Self-serve: ${s.subscription.enableSelfServeSeats ? 'YES' : 'no'}`);
@@ -10201,6 +10429,89 @@ async function handleTool(name, args) {
10201
10429
  return JSON.stringify(listing, null, 2);
10202
10430
  }
10203
10431
 
10432
+ case 'recruiting_ats_failures': {
10433
+ const rows = await messagingGet('/Recruiting/Applications/AtsFailures');
10434
+ return JSON.stringify(rows, null, 2);
10435
+ }
10436
+ case 'recruiting_ats_retry': {
10437
+ const id = Number(args.applicationId);
10438
+ if (!Number.isInteger(id) || id <= 0) throw new Error('applicationId must be a positive integer.');
10439
+ const r = await messagingPost(`/Recruiting/Applications/${id}/AtsRetry`, {});
10440
+ return JSON.stringify(r, null, 2);
10441
+ }
10442
+ case 'recruiting_apply_settings': {
10443
+ const body = {};
10444
+ if (args.publicApplyEnabled !== undefined) body.publicApplyEnabled = Boolean(args.publicApplyEnabled);
10445
+ if (args.autoCreateCaseTypeId !== undefined) {
10446
+ const n = Number(args.autoCreateCaseTypeId);
10447
+ if (!Number.isInteger(n) || n < 0) throw new Error('autoCreateCaseTypeId must be an integer >= 0 (0 clears).');
10448
+ body.autoCreateCaseTypeId = n;
10449
+ }
10450
+ if (args.defaultListingDays !== undefined) {
10451
+ const d = Number(args.defaultListingDays);
10452
+ if (!Number.isInteger(d) || d < 1 || d > 365) throw new Error('defaultListingDays must be an integer from 1 to 365.');
10453
+ body.defaultListingDays = d;
10454
+ }
10455
+ if (!Object.keys(body).length) throw new Error('Specify publicApplyEnabled, autoCreateCaseTypeId and/or defaultListingDays.');
10456
+ const r = await messagingPut('/Recruiting/ApplySettings', body);
10457
+ return JSON.stringify(r, null, 2);
10458
+ }
10459
+ case 'recruiting_bot_settings': {
10460
+ const fields = ['pageContextEnabled', 'revealPostedDate', 'revealDescription', 'revealApplyLink', 'publicApplyEnabled', 'applyMode', 'maxResults', 'greeting', 'tone'];
10461
+ const body = {};
10462
+ for (const f of fields) if (args[f] !== undefined) body[f] = args[f];
10463
+ if (body.applyMode !== undefined && !['both', 'link', 'inproduct'].includes(String(body.applyMode).toLowerCase())) throw new Error('applyMode must be both, link or inproduct.');
10464
+ if (body.tone !== undefined && !['friendly', 'professional', 'concise'].includes(String(body.tone).toLowerCase())) throw new Error('tone must be friendly, professional or concise.');
10465
+ 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.');
10466
+ const r = Object.keys(body).length ? await messagingPut('/Recruiting/BotSettings', body) : await messagingGet('/Recruiting/BotSettings');
10467
+ return JSON.stringify(r, null, 2);
10468
+ }
10469
+ case 'recruiting_download_resume': {
10470
+ const id = Number(args.applicationId);
10471
+ if (!Number.isInteger(id) || id <= 0) throw new Error('applicationId must be a positive integer.');
10472
+ if (!MESSAGING_API) throw new Error('VELARO_MESSAGING_API is not configured.');
10473
+ const res = await fetch(`${MESSAGING_API}/Recruiting/Applications/${id}/Resume`, {
10474
+ headers: { Authorization: authHeader() },
10475
+ signal: AbortSignal.timeout(30000),
10476
+ });
10477
+ if (!res.ok) {
10478
+ const text = await res.text().catch(() => '');
10479
+ throw new Error(`/Recruiting/Applications/${id}/Resume -> ${res.status}: ${text.slice(0, 300)}`);
10480
+ }
10481
+ const buf = Buffer.from(await res.arrayBuffer());
10482
+ 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}`);
10483
+ const cd = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(res.headers.get('content-disposition') || '');
10484
+ return JSON.stringify({
10485
+ fileName: cd ? decodeURIComponent(cd[1]) : `resume-${id}`,
10486
+ contentType: res.headers.get('content-type') || 'application/octet-stream',
10487
+ sizeBytes: buf.length,
10488
+ base64: buf.toString('base64'),
10489
+ }, null, 2);
10490
+ }
10491
+
10492
+ case 'jobboard_lifecycle_status': {
10493
+ const r = await messagingGet('/Recruiting/Lifecycle');
10494
+ return JSON.stringify(r, null, 2);
10495
+ }
10496
+ case 'jobboard_close_listing': {
10497
+ const id = Number(args.listingId);
10498
+ if (!Number.isInteger(id) || id <= 0) throw new Error('listingId must be a positive integer.');
10499
+ const r = await messagingPost(`/Recruiting/Jobs/${id}/close`, {});
10500
+ return JSON.stringify(r, null, 2);
10501
+ }
10502
+ case 'jobboard_renew_listing': {
10503
+ const id = Number(args.listingId);
10504
+ if (!Number.isInteger(id) || id <= 0) throw new Error('listingId must be a positive integer.');
10505
+ const body = {};
10506
+ if (args.days !== undefined) {
10507
+ const d = Number(args.days);
10508
+ if (!Number.isInteger(d) || d < 1 || d > 365) throw new Error('days must be an integer from 1 to 365.');
10509
+ body.days = d;
10510
+ }
10511
+ const r = await messagingPost(`/Recruiting/Jobs/${id}/renew`, body);
10512
+ return JSON.stringify(r, null, 2);
10513
+ }
10514
+
10204
10515
  case 'jobboard_move_application_stage': {
10205
10516
  const app = await messagingPut(`/Recruiting/Applications/${args.applicationId}/Stage`, { stage: args.stage });
10206
10517
  return JSON.stringify(app, null, 2);