@velaro/mcp-server 0.6.55 → 0.6.56

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 +223 -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.56",
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',
@@ -4583,6 +4738,11 @@ async function handleTool(name, args) {
4583
4738
  return JSON.stringify(result, null, 2);
4584
4739
  }
4585
4740
 
4741
+ case 'site_scraper_usage': {
4742
+ const result = await api('GET', `ScraperJobs/StaffUsage/${encodeURIComponent(args.siteId)}`);
4743
+ return JSON.stringify(result, null, 2);
4744
+ }
4745
+
4586
4746
  case 'apple_health': {
4587
4747
  const h = await messagingGet(`/SupportTools/sites/${args.siteId}/apple/health`);
4588
4748
  return JSON.stringify(h, null, 2);
@@ -5848,6 +6008,53 @@ async function handleTool(name, args) {
5848
6008
  return lines.join('\n');
5849
6009
  }
5850
6010
 
6011
+ case 'index_move_preview': {
6012
+ const { fromIndex, toIndex, kind, skipBlocked, selector } = args;
6013
+ const r = await messagingPost('/IndexMove/Preview', { fromIndex, toIndex, kind, skipBlocked, selector });
6014
+ return JSON.stringify(r, null, 2);
6015
+ }
6016
+ case 'index_move_execute': {
6017
+ const { fromIndex, toIndex, kind, skipBlocked, selector } = args;
6018
+ const r = await messagingPost('/IndexMove/Execute', { fromIndex, toIndex, kind, skipBlocked, selector });
6019
+ return JSON.stringify(r, null, 2);
6020
+ }
6021
+ case 'index_split_preview': {
6022
+ const body = { fromIndex: args.fromIndex, toIndex: args.toIndex, docIds: args.docIds, skipBlocked: args.skipBlocked };
6023
+ const r = await messagingPost('/IndexMove/Split/Preview', body);
6024
+ return JSON.stringify(r, null, 2);
6025
+ }
6026
+ case 'index_split_execute': {
6027
+ const body = { fromIndex: args.fromIndex, toIndex: args.toIndex, docIds: args.docIds, skipBlocked: args.skipBlocked };
6028
+ const r = await messagingPost('/IndexMove/Split/Execute', body);
6029
+ return JSON.stringify(r, null, 2);
6030
+ }
6031
+ case 'index_move_status': {
6032
+ const r = await messagingGet(args.moveId
6033
+ ? `/IndexMove/Moves/${encodeURIComponent(args.moveId)}`
6034
+ : '/IndexMove/Moves?take=25');
6035
+ return JSON.stringify(r, null, 2);
6036
+ }
6037
+ case 'index_move_control': {
6038
+ const verb = { resume: 'Resume', abort: 'Abort', undo: 'Undo' }[args.action];
6039
+ if (!verb) throw new Error('action must be resume, abort or undo');
6040
+ const r = await messagingPost(`/IndexMove/Moves/${encodeURIComponent(args.moveId)}/${verb}`);
6041
+ return JSON.stringify(r, null, 2);
6042
+ }
6043
+ case 'index_mixed_content': {
6044
+ const r = await messagingGet(`/IndexMove/MixedContent?index=${encodeURIComponent(args.indexName)}`);
6045
+ return JSON.stringify(r, null, 2);
6046
+ }
6047
+ case 'index_integrity_check': {
6048
+ const r = await messagingGet(`/IndexMove/Integrity?index=${encodeURIComponent(args.indexName)}`);
6049
+ return JSON.stringify(r, null, 2);
6050
+ }
6051
+ case 'index_integrity_repair': {
6052
+ const r = await messagingPost('/IndexMove/Integrity/Repair', {
6053
+ indexName: args.indexName,
6054
+ removeDuplicateRecords: !!args.removeDuplicateRecords,
6055
+ });
6056
+ return JSON.stringify(r, null, 2);
6057
+ }
5851
6058
  case 'index_status': {
5852
6059
  const sources = await api('GET', '/AzureIndexes/IntegrationSources');
5853
6060
  if (!Array.isArray(sources) || !sources.length) return 'No indexed sources found. Run index_ingest to populate.';
@@ -6737,7 +6944,7 @@ async function handleTool(name, args) {
6737
6944
 
6738
6945
  case 'support_user_lookup': {
6739
6946
  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}".`;
6947
+ 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
6948
  return users.map(u => {
6742
6949
  const roles = u.sites?.map(s =>
6743
6950
  ` site ${s.siteId} (${s.companyName}) — ${s.isAdministrator ? 'Admin' : s.isManager ? 'Manager' : 'Agent'}${s.isActive === false ? ' INACTIVE' : ''}`
@@ -6753,7 +6960,7 @@ async function handleTool(name, args) {
6753
6960
 
6754
6961
  case 'support_site_lookup': {
6755
6962
  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}".`;
6963
+ 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
6964
  return sites.map(s => {
6758
6965
  const admins = s.admins?.map(a => ` ${a.userName} (${a.firstName} ${a.lastName})`).join('\n') ?? 'none';
6759
6966
  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 +7057,14 @@ async function handleTool(name, args) {
6850
7057
  return lines.join('\n');
6851
7058
  }
6852
7059
 
7060
+ case 'tour_insights': {
7061
+ const days = Math.min(365, Math.max(1, args.days ?? 30));
7062
+ const path = args.siteId
7063
+ ? `/SupportTools/sites/${args.siteId}/tour-events?days=${days}`
7064
+ : `/SupportTools/tour-insights?days=${days}`;
7065
+ return JSON.stringify(await api('GET', path), null, 2);
7066
+ }
7067
+
6853
7068
  case 'support_extend_subscription': {
6854
7069
  const res = await api('POST', `/SupportTools/sites/${args.siteId}/extend-subscription`, { days: args.days });
6855
7070
  return res?.success
@@ -7429,6 +7644,12 @@ async function handleTool(name, args) {
7429
7644
  return lines.join('\n');
7430
7645
  }
7431
7646
 
7647
+ case 'magento_diagnose': {
7648
+ if (!args.siteId) return 'siteId is required.';
7649
+ const r = await api('GET', `/diagnostics/test-magento?siteId=${encodeURIComponent(args.siteId)}`);
7650
+ return JSON.stringify(r, null, 2);
7651
+ }
7652
+
7432
7653
  // ── CallRail Attribution ──────────────────────────────────────────────────
7433
7654
  case 'callrail_get_attributions': {
7434
7655
  const limit = Math.min(100, args.limit ?? 25);