@velaro/mcp-server 0.6.55 → 0.6.57

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 +7 -3
  2. package/server.js +253 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "0.6.55",
3
+ "version": "0.6.57",
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": {
@@ -14,7 +14,8 @@
14
14
  "qs": "^6.16.0"
15
15
  },
16
16
  "overrides": {
17
- "qs": "^6.16.0"
17
+ "qs": "^6.16.0",
18
+ "hono": "^4.13.5"
18
19
  },
19
20
  "scripts": {
20
21
  "start": "node server.js"
@@ -43,5 +44,8 @@
43
44
  "server.js",
44
45
  "server.json",
45
46
  "README.md"
46
- ]
47
+ ],
48
+ "devDependencies": {
49
+ "license-checker-rseidelsohn": "^4.4.2"
50
+ }
47
51
  }
package/server.js CHANGED
@@ -62,6 +62,12 @@ const MESSAGING_API = process.env.VELARO_MESSAGING_API || null;
62
62
  const MCP_KEY = process.env.VELARO_MCP_KEY;
63
63
  const JWT = process.env.VELARO_JWT;
64
64
 
65
+ // mcp/staff-server.js defaults to staging instead -- the two servers' defaults intentionally
66
+ // differ, but a zero-result lookup gives no hint which environment was actually searched
67
+ // either way. See ENV_LABEL in staff-server.js for the incident this class of confusion caused
68
+ // (a real production customer read as "doesn't exist" from a staging-scoped search).
69
+ const ENV_LABEL = API_BASE.includes('staging') ? 'staging' : 'production';
70
+
65
71
  // Known admin API hosts, for tools that let a caller explicitly target one environment
66
72
  // regardless of what this server process's own API_BASE is configured to (see the `env` param
67
73
  // on the calendly_* superadmin tools below). Deliberately NOT used as a default anywhere —
@@ -561,6 +567,18 @@ const TOOLS = [
561
567
  },
562
568
  },
563
569
 
570
+ {
571
+ name: 'site_scraper_usage',
572
+ description: 'Velaro staff only. Scraper/upload/chunk consumption for ANY site in one call: pages this month vs budget (both the admin and messaging ledgers), upload pages, OCR pages, jobs today vs daily cap, and chunks indexed vs MaxIndexedChunkTotal with chunks grouped by index name. Wraps GET ScraperJobs/StaffUsage/{siteId} (IsVelaroAdminAsync + super-admin enforced server-side). Read-only.',
573
+ inputSchema: {
574
+ type: 'object',
575
+ properties: {
576
+ siteId: { type: 'number', description: 'Target site ID (need not be your own)' },
577
+ },
578
+ required: ['siteId'],
579
+ },
580
+ },
581
+
564
582
  // ── Startup migration diagnostics (AdminStartupDiagnostics canary table) ──
565
583
  {
566
584
  name: 'admin_startup_diagnostics',
@@ -1593,6 +1611,120 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1593
1611
  description: 'Show what is currently indexed for the site: document counts per source type (kb_article, scraper_page, bigcommerce_product, etc). Use to verify ingestion completed and data is present.',
1594
1612
  inputSchema: { type: 'object', properties: {} },
1595
1613
  },
1614
+ {
1615
+ name: 'index_move_preview',
1616
+ description: 'Preview moving (or copying) documents between two knowledge indexes on the site. Writes nothing. Returns document/chunk/page counts, per-document method (copy_vectors, rechunk, already_in_target, blocked), quota effect, and whether it can run. No rescan and no OCR is ever needed. Moving into a troubleshooting index needs the EnableTroubleshootingKnowledge entitlement. Wraps POST IndexMove/Preview (velaro-messaging); the site comes from the caller auth, never an argument.',
1617
+ inputSchema: {
1618
+ type: 'object',
1619
+ properties: {
1620
+ fromIndex: { type: 'string', description: 'Source index name' },
1621
+ toIndex: { type: 'string', description: 'Target index name' },
1622
+ kind: { type: 'string', enum: ['move', 'copy'], description: 'Default move' },
1623
+ skipBlocked: { type: 'boolean', description: 'Leave blocked documents in place and move the rest' },
1624
+ selector: {
1625
+ type: 'object',
1626
+ description: 'What to move. Needs at least one field or all:true. Fields: all (bool), jobId, urlPrefix, sourceIds[], fileNames[], docIds[] (numbers), sourceType (website_page | video_transcript | uploaded_document | onedrive_file | custom_content).',
1627
+ },
1628
+ },
1629
+ required: ['fromIndex', 'toIndex', 'selector'],
1630
+ },
1631
+ },
1632
+ {
1633
+ name: 'index_move_execute',
1634
+ description: 'Run a move/copy between knowledge indexes. Always call index_move_preview first and check canExecute. Write-then-delete and resumable: content is never missing from both indexes. Returns a moveId; poll index_move_status. Wraps POST IndexMove/Execute.',
1635
+ inputSchema: {
1636
+ type: 'object',
1637
+ properties: {
1638
+ fromIndex: { type: 'string' },
1639
+ toIndex: { type: 'string' },
1640
+ kind: { type: 'string', enum: ['move', 'copy'] },
1641
+ skipBlocked: { type: 'boolean' },
1642
+ selector: {
1643
+ type: 'object',
1644
+ description: 'What to move. Needs at least one field or all:true. Fields: all (bool), jobId, urlPrefix, sourceIds[], fileNames[], docIds[] (numbers), sourceType (website_page | video_transcript | uploaded_document | onedrive_file | custom_content).',
1645
+ },
1646
+ },
1647
+ required: ['fromIndex', 'toIndex', 'selector'],
1648
+ },
1649
+ },
1650
+ {
1651
+ name: 'index_split_preview',
1652
+ description: 'Detect manual-like documents in a general index and preview moving them into an existing troubleshooting-purpose index. Writes nothing. Wraps POST IndexMove/Split/Preview. Use index_split_execute to run it.',
1653
+ inputSchema: {
1654
+ type: 'object',
1655
+ properties: {
1656
+ fromIndex: { type: 'string' },
1657
+ toIndex: { type: 'string', description: 'Existing troubleshooting-purpose index' },
1658
+ docIds: { type: 'array', items: { type: 'number' }, description: 'Only these documents (default: every detected manual)' },
1659
+ skipBlocked: { type: 'boolean' },
1660
+ },
1661
+ required: ['fromIndex', 'toIndex'],
1662
+ },
1663
+ },
1664
+ {
1665
+ name: 'index_split_execute',
1666
+ description: 'Run a split: move detected manuals from a general index into an existing troubleshooting-purpose index. Call index_split_preview first. Resumable and undoable via index_move_control. Wraps POST IndexMove/Split/Execute.',
1667
+ inputSchema: {
1668
+ type: 'object',
1669
+ properties: {
1670
+ fromIndex: { type: 'string' },
1671
+ toIndex: { type: 'string' },
1672
+ docIds: { type: 'array', items: { type: 'number' } },
1673
+ skipBlocked: { type: 'boolean' },
1674
+ },
1675
+ required: ['fromIndex', 'toIndex'],
1676
+ },
1677
+ },
1678
+ {
1679
+ name: 'index_move_status',
1680
+ description: 'Status of one move (moveId) or the recent moves (omit moveId): status, per-document progress, canResume/canAbort/canUndo. Wraps GET IndexMove/Moves[/{id}].',
1681
+ inputSchema: {
1682
+ type: 'object',
1683
+ properties: { moveId: { type: 'string' } },
1684
+ },
1685
+ },
1686
+ {
1687
+ name: 'index_move_control',
1688
+ description: 'Resume a stopped move, abort a queued/failed move that has not started removing content, or undo a completed move. Wraps POST IndexMove/Moves/{id}/Resume|Abort|Undo.',
1689
+ inputSchema: {
1690
+ type: 'object',
1691
+ properties: {
1692
+ moveId: { type: 'string' },
1693
+ action: { type: 'string', enum: ['resume', 'abort', 'undo'] },
1694
+ },
1695
+ required: ['moveId', 'action'],
1696
+ },
1697
+ },
1698
+ {
1699
+ name: 'index_mixed_content',
1700
+ description: 'Check whether a general index holds manual-like documents that belong in a Troubleshooting Assistant index. Read-only. Wraps GET IndexMove/MixedContent.',
1701
+ inputSchema: {
1702
+ type: 'object',
1703
+ properties: { indexName: { type: 'string' } },
1704
+ required: ['indexName'],
1705
+ },
1706
+ },
1707
+ {
1708
+ name: 'index_integrity_check',
1709
+ description: 'Read-only integrity report for a knowledge index: orphan chunks, records without chunks, count mismatches, duplicate records. Wraps GET IndexMove/Integrity.',
1710
+ inputSchema: {
1711
+ type: 'object',
1712
+ properties: { indexName: { type: 'string' } },
1713
+ required: ['indexName'],
1714
+ },
1715
+ },
1716
+ {
1717
+ name: 'index_integrity_repair',
1718
+ description: 'Repair a knowledge index. Additive and idempotent: corrects counts and adopts orphan chunks into tracking rows, never deletes vectors. removeDuplicateRecords also removes duplicate tracking rows. Run index_integrity_check first. Wraps POST IndexMove/Integrity/Repair.',
1719
+ inputSchema: {
1720
+ type: 'object',
1721
+ properties: {
1722
+ indexName: { type: 'string' },
1723
+ removeDuplicateRecords: { type: 'boolean' },
1724
+ },
1725
+ required: ['indexName'],
1726
+ },
1727
+ },
1596
1728
  {
1597
1729
  name: 'index_ingest',
1598
1730
  description: 'Trigger ingestion of a scraper job into the knowledge index. Fires in the background; use index_status after ~2 minutes to verify chunks appeared.',
@@ -2331,6 +2463,17 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2331
2463
  required: ['siteId'],
2332
2464
  },
2333
2465
  },
2466
+ {
2467
+ name: 'tour_insights',
2468
+ description: 'Moshky setup-tour funnel and feedback (Velaro staff only). Without siteId: per-tour, per-step counts of offered/opened/step_viewed/arrived/stuck/completed/dismissed/load_error over the last N days plus recent scrubbed feedback. With siteId: that one site\'s tour event timeline and feedback. Feedback comments are PII-scrubbed and for staff eyes only.',
2469
+ inputSchema: {
2470
+ type: 'object',
2471
+ properties: {
2472
+ siteId: { type: 'number', description: 'Optional. When set, returns that site\'s tour timeline instead of the cross-site funnel.' },
2473
+ days: { type: 'number', description: 'Look-back window in days (default 30, max 365)' },
2474
+ },
2475
+ },
2476
+ },
2334
2477
  {
2335
2478
  name: 'support_extend_subscription',
2336
2479
  description: 'Extend a customer site\'s subscription expiration by N days. SuperAdmin only.',
@@ -2935,6 +3078,18 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2935
3078
  },
2936
3079
  },
2937
3080
 
3081
+ {
3082
+ name: 'magento_diagnose',
3083
+ description: 'Run the Magento live-connection diagnostic for a site: config present, access token valid, /rest/V1 reachability, store code/base URL, and ACL scope probes. Calls messaging GET diagnostics/test-magento?siteId=<id>. Read-only.',
3084
+ inputSchema: {
3085
+ type: 'object',
3086
+ properties: {
3087
+ siteId: { type: 'number', description: 'Site to diagnose (e.g. 1032)' },
3088
+ },
3089
+ required: ['siteId'],
3090
+ },
3091
+ },
3092
+
2938
3093
  // ── CallRail Attribution ──────────────────────────────────────────────────────
2939
3094
  {
2940
3095
  name: 'callrail_get_attributions',
@@ -2999,6 +3154,22 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2999
3154
  },
3000
3155
  },
3001
3156
 
3157
+ {
3158
+ name: 'netsuite_m2m_init',
3159
+ description: 'Start NetSuite machine-to-machine (OAuth 2.0 Client Credentials) setup: generates the Velaro public certificate PEM to upload in NetSuite (Setup > Integration > Manage Authentication > OAuth 2.0 Client Credentials (M2M) Setup) and returns the steps. Omit configId for a new connection (company then required). Lives in velaro-messaging - requires VELARO_MESSAGING_API.',
3160
+ inputSchema: { type: 'object', properties: { configId: { type: 'number', description: 'Existing NetSuite config ID (rotate/switch).' }, company: { type: 'string', description: 'NetSuite account ID for a new connection (sandbox looks like 1234567_SB1).' }, displayName: { type: 'string' } } },
3161
+ },
3162
+ {
3163
+ name: 'netsuite_m2m_save',
3164
+ description: 'Save the Client ID (NetSuite integration record) and Certificate ID (NetSuite M2M mapping) for a NetSuite config after netsuite_m2m_init. Lives in velaro-messaging - requires VELARO_MESSAGING_API.',
3165
+ inputSchema: { type: 'object', properties: { configId: { type: 'number' }, clientId: { type: 'string' }, certificateId: { type: 'string' } }, required: ['configId', 'clientId', 'certificateId'] },
3166
+ },
3167
+ {
3168
+ name: 'netsuite_m2m_verify',
3169
+ description: 'Verify a NetSuite M2M connection with per-check results (token, REST, customer read) and specific failure reasons and fixes. Lives in velaro-messaging - requires VELARO_MESSAGING_API.',
3170
+ inputSchema: { type: 'object', properties: { configId: { type: 'number' } }, required: ['configId'] },
3171
+ },
3172
+
3002
3173
  // ── Microsoft Teams Phone (config CRUD) ────────────────────────────────────
3003
3174
  // Live in-call actions (hold/resume/end/transfer, TeamsPhoneCtiController) are
3004
3175
  // intentionally NOT exposed here — see mcp-cli-exempt comments on that controller.
@@ -4583,6 +4754,11 @@ async function handleTool(name, args) {
4583
4754
  return JSON.stringify(result, null, 2);
4584
4755
  }
4585
4756
 
4757
+ case 'site_scraper_usage': {
4758
+ const result = await api('GET', `ScraperJobs/StaffUsage/${encodeURIComponent(args.siteId)}`);
4759
+ return JSON.stringify(result, null, 2);
4760
+ }
4761
+
4586
4762
  case 'apple_health': {
4587
4763
  const h = await messagingGet(`/SupportTools/sites/${args.siteId}/apple/health`);
4588
4764
  return JSON.stringify(h, null, 2);
@@ -5848,6 +6024,53 @@ async function handleTool(name, args) {
5848
6024
  return lines.join('\n');
5849
6025
  }
5850
6026
 
6027
+ case 'index_move_preview': {
6028
+ const { fromIndex, toIndex, kind, skipBlocked, selector } = args;
6029
+ const r = await messagingPost('/IndexMove/Preview', { fromIndex, toIndex, kind, skipBlocked, selector });
6030
+ return JSON.stringify(r, null, 2);
6031
+ }
6032
+ case 'index_move_execute': {
6033
+ const { fromIndex, toIndex, kind, skipBlocked, selector } = args;
6034
+ const r = await messagingPost('/IndexMove/Execute', { fromIndex, toIndex, kind, skipBlocked, selector });
6035
+ return JSON.stringify(r, null, 2);
6036
+ }
6037
+ case 'index_split_preview': {
6038
+ const body = { fromIndex: args.fromIndex, toIndex: args.toIndex, docIds: args.docIds, skipBlocked: args.skipBlocked };
6039
+ const r = await messagingPost('/IndexMove/Split/Preview', body);
6040
+ return JSON.stringify(r, null, 2);
6041
+ }
6042
+ case 'index_split_execute': {
6043
+ const body = { fromIndex: args.fromIndex, toIndex: args.toIndex, docIds: args.docIds, skipBlocked: args.skipBlocked };
6044
+ const r = await messagingPost('/IndexMove/Split/Execute', body);
6045
+ return JSON.stringify(r, null, 2);
6046
+ }
6047
+ case 'index_move_status': {
6048
+ const r = await messagingGet(args.moveId
6049
+ ? `/IndexMove/Moves/${encodeURIComponent(args.moveId)}`
6050
+ : '/IndexMove/Moves?take=25');
6051
+ return JSON.stringify(r, null, 2);
6052
+ }
6053
+ case 'index_move_control': {
6054
+ const verb = { resume: 'Resume', abort: 'Abort', undo: 'Undo' }[args.action];
6055
+ if (!verb) throw new Error('action must be resume, abort or undo');
6056
+ const r = await messagingPost(`/IndexMove/Moves/${encodeURIComponent(args.moveId)}/${verb}`);
6057
+ return JSON.stringify(r, null, 2);
6058
+ }
6059
+ case 'index_mixed_content': {
6060
+ const r = await messagingGet(`/IndexMove/MixedContent?index=${encodeURIComponent(args.indexName)}`);
6061
+ return JSON.stringify(r, null, 2);
6062
+ }
6063
+ case 'index_integrity_check': {
6064
+ const r = await messagingGet(`/IndexMove/Integrity?index=${encodeURIComponent(args.indexName)}`);
6065
+ return JSON.stringify(r, null, 2);
6066
+ }
6067
+ case 'index_integrity_repair': {
6068
+ const r = await messagingPost('/IndexMove/Integrity/Repair', {
6069
+ indexName: args.indexName,
6070
+ removeDuplicateRecords: !!args.removeDuplicateRecords,
6071
+ });
6072
+ return JSON.stringify(r, null, 2);
6073
+ }
5851
6074
  case 'index_status': {
5852
6075
  const sources = await api('GET', '/AzureIndexes/IntegrationSources');
5853
6076
  if (!Array.isArray(sources) || !sources.length) return 'No indexed sources found. Run index_ingest to populate.';
@@ -6737,7 +6960,7 @@ async function handleTool(name, args) {
6737
6960
 
6738
6961
  case 'support_user_lookup': {
6739
6962
  const users = await api('GET', `/SupportTools/users/search?q=${encodeURIComponent(args.query)}`);
6740
- if (!Array.isArray(users) || !users.length) return `No users found matching "${args.query}".`;
6963
+ if (!Array.isArray(users) || !users.length) return `No users found matching "${args.query}" in ${ENV_LABEL}. If this looks like a real customer, re-run with VELARO_ADMIN_API set to the other environment before concluding they don't exist.`;
6741
6964
  return users.map(u => {
6742
6965
  const roles = u.sites?.map(s =>
6743
6966
  ` site ${s.siteId} (${s.companyName}) — ${s.isAdministrator ? 'Admin' : s.isManager ? 'Manager' : 'Agent'}${s.isActive === false ? ' INACTIVE' : ''}`
@@ -6753,7 +6976,7 @@ async function handleTool(name, args) {
6753
6976
 
6754
6977
  case 'support_site_lookup': {
6755
6978
  const sites = await api('GET', `/SupportTools/sites/search?q=${encodeURIComponent(args.query)}`);
6756
- if (!Array.isArray(sites) || !sites.length) return `No sites found matching "${args.query}".`;
6979
+ if (!Array.isArray(sites) || !sites.length) return `No sites found matching "${args.query}" in ${ENV_LABEL}. If this looks like a real customer, re-run with VELARO_ADMIN_API set to the other environment before concluding they don't exist.`;
6757
6980
  return sites.map(s => {
6758
6981
  const admins = s.admins?.map(a => ` ${a.userName} (${a.firstName} ${a.lastName})`).join('\n') ?? 'none';
6759
6982
  return `[siteId=${s.siteId}] ${s.companyName}${s.isTestSite ? ' [TEST]' : ''}\n URL: ${s.companyURL ?? '—'} | Created: ${s.dateCreated ? s.dateCreated.slice(0,10) : '—'}\n Admins:\n${admins}`;
@@ -6850,6 +7073,14 @@ async function handleTool(name, args) {
6850
7073
  return lines.join('\n');
6851
7074
  }
6852
7075
 
7076
+ case 'tour_insights': {
7077
+ const days = Math.min(365, Math.max(1, args.days ?? 30));
7078
+ const path = args.siteId
7079
+ ? `/SupportTools/sites/${args.siteId}/tour-events?days=${days}`
7080
+ : `/SupportTools/tour-insights?days=${days}`;
7081
+ return JSON.stringify(await api('GET', path), null, 2);
7082
+ }
7083
+
6853
7084
  case 'support_extend_subscription': {
6854
7085
  const res = await api('POST', `/SupportTools/sites/${args.siteId}/extend-subscription`, { days: args.days });
6855
7086
  return res?.success
@@ -7429,6 +7660,12 @@ async function handleTool(name, args) {
7429
7660
  return lines.join('\n');
7430
7661
  }
7431
7662
 
7663
+ case 'magento_diagnose': {
7664
+ if (!args.siteId) return 'siteId is required.';
7665
+ const r = await api('GET', `/diagnostics/test-magento?siteId=${encodeURIComponent(args.siteId)}`);
7666
+ return JSON.stringify(r, null, 2);
7667
+ }
7668
+
7432
7669
  // ── CallRail Attribution ──────────────────────────────────────────────────
7433
7670
  case 'callrail_get_attributions': {
7434
7671
  const limit = Math.min(100, args.limit ?? 25);
@@ -7489,6 +7726,20 @@ async function handleTool(name, args) {
7489
7726
  return `CTI token generated/rotated.\nDial URL: ${urls.dialUrlTemplate}\nScreen-pop URL: ${urls.screenPopUrl}`;
7490
7727
  }
7491
7728
 
7729
+ case 'netsuite_m2m_init': {
7730
+ const r = await messagingPost('/NetSuite/M2M/Init', { id: args.configId ?? null, company: args.company, displayName: args.displayName });
7731
+ return `Config ${r.id ?? args.configId ?? 'new'}. Upload this certificate in NetSuite (Setup > Integration > Manage Authentication > OAuth 2.0 Client Credentials (M2M) Setup):\n${r.certificatePem ?? '(none returned)'}\n${Array.isArray(r.steps) ? r.steps.map((x, i) => `${i + 1}. ${x}`).join('\n') : ''}`;
7732
+ }
7733
+ case 'netsuite_m2m_save': {
7734
+ await messagingPost('/NetSuite/M2M/Save', { id: args.configId, clientId: args.clientId, certificateId: args.certificateId });
7735
+ return `Saved for config ${args.configId}. Run netsuite_m2m_verify next.`;
7736
+ }
7737
+ case 'netsuite_m2m_verify': {
7738
+ const r = await messagingPost('/NetSuite/M2M/Verify', { id: args.configId });
7739
+ const checks = Array.isArray(r.checks) ? r.checks : [];
7740
+ return checks.map(c => `${c.ok ? 'PASS' : 'FAIL'} ${c.name}${c.ok ? '' : ` - ${c.reason || c.reasonCode || ''}${c.howToFix ? ` | Fix: ${c.howToFix}` : ''}`}`).join('\n') || 'No checks returned.';
7741
+ }
7742
+
7492
7743
  // ── Microsoft Teams Phone (config CRUD) ───────────────────────────────────
7493
7744
  case 'teams_phone_get_config': {
7494
7745
  const config = await messagingGet('/TeamsPhone/Config');