@velaro/mcp-server 0.6.58 → 0.6.60

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 +102 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "0.6.58",
3
+ "version": "0.6.60",
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
@@ -245,19 +245,73 @@ function mcpFindUnresolved(value) {
245
245
  .map(([k]) => k);
246
246
  }
247
247
 
248
+ // The hosted gateway requires an initialized session (Mcp-Session-Id + MCP-Protocol-Version) on
249
+ // every call after "initialize". Cache one per process and re-initialize when it expires (404).
250
+ let gatewaySession = null;
251
+ async function gatewayInit() {
252
+ const res = await fetch(`${API_BASE}/mcp`, {
253
+ method: 'POST',
254
+ headers: { Authorization: authHeader(), 'Content-Type': 'application/json' },
255
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'velaro-mcp-server', version: '1' } } }),
256
+ });
257
+ if (!res.ok) {
258
+ const text = await res.text().catch(() => '');
259
+ throw new Error(`Velaro MCP gateway initialize -> ${res.status}: ${text.slice(0, 300)}`);
260
+ }
261
+ const sid = res.headers.get('mcp-session-id');
262
+ if (!sid) throw new Error('Velaro MCP gateway initialize returned no Mcp-Session-Id');
263
+ gatewaySession = { sid, version: res.headers.get('mcp-protocol-version') || '2025-11-25' };
264
+ return gatewaySession;
265
+ }
266
+
267
+ // Tools served by velaro-messaging's admin-tools gateway (McpAdminGatewayController, POST /mcp/v1/admin-tools/call,
268
+ // no session). The admin /mcp gateway only serves kb_* tools, so these used to fail there.
269
+ const MESSAGING_ADMIN_TOOLS = new Set([
270
+ 'workflow_create', 'workflow_list', 'workflow_get', 'workflow_toggle',
271
+ 'bot_create', 'bot_list', 'bot_get', 'bot_update_prompt', 'bot_set_knowledge',
272
+ 'ai_config_attach_index', 'call_pop_settings_get', 'call_pop_settings_set',
273
+ 'agent_list', 'team_list', 'conversation_list', 'conversation_get', 'conversation_search',
274
+ 'contact_search', 'contact_get', 'routing_list', 'routing_get', 'site_info',
275
+ 'ms_bookings_cancel_appointment', 'ms_bookings_check_availability_range', 'ms_bookings_reschedule_appointment',
276
+ 'outlook_calendar_cancel', 'outlook_calendar_get_available_range',
277
+ ]);
278
+
279
+ async function messagingAdminRpc(toolName, args) {
280
+ if (!MESSAGING_API) throw new Error('VELARO_MESSAGING_API is not configured.');
281
+ const res = await fetch(`${MESSAGING_API}/mcp/v1/admin-tools/call`, {
282
+ method: 'POST',
283
+ headers: { Authorization: authHeader(), 'Content-Type': 'application/json' },
284
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: toolName, arguments: args ?? {} } }),
285
+ signal: AbortSignal.timeout(30000),
286
+ });
287
+ const data = await res.json().catch(() => ({}));
288
+ if (!res.ok || data.error) {
289
+ throw new Error(`Velaro admin-tools ${toolName} -> ${res.status}: ${data.error?.message || JSON.stringify(data).slice(0, 300)}`);
290
+ }
291
+ const content = data.result?.content?.[0]?.text;
292
+ if (content === undefined) throw new Error('Empty response from admin-tools gateway');
293
+ return content;
294
+ }
295
+
248
296
  // Call the hosted MCP gateway (JSON-RPC) for tools that are dispatched server-side.
249
297
  async function rpc(toolName, args) {
298
+ if (MESSAGING_ADMIN_TOOLS.has(toolName)) return messagingAdminRpc(toolName, args);
250
299
  const body = {
251
300
  jsonrpc: '2.0',
252
301
  id: 1,
253
302
  method: 'tools/call',
254
303
  params: { name: toolName, arguments: args ?? {} },
255
304
  };
256
- const res = await fetch(`${API_BASE}/mcp`, {
257
- method: 'POST',
258
- headers: { Authorization: authHeader(), 'Content-Type': 'application/json' },
259
- body: JSON.stringify(body),
260
- });
305
+ const send = async () => {
306
+ const s = gatewaySession || await gatewayInit();
307
+ return fetch(`${API_BASE}/mcp`, {
308
+ method: 'POST',
309
+ headers: { Authorization: authHeader(), 'Content-Type': 'application/json', 'Mcp-Session-Id': s.sid, 'MCP-Protocol-Version': s.version },
310
+ body: JSON.stringify(body),
311
+ });
312
+ };
313
+ let res = await send();
314
+ if (res.status === 404) { gatewaySession = null; res = await send(); }
261
315
  if (!res.ok) {
262
316
  const text = await res.text().catch(() => '');
263
317
  throw new Error(`Velaro MCP gateway ${toolName} -> ${res.status}: ${text.slice(0, 300)}`);
@@ -581,6 +635,12 @@ const TOOLS = [
581
635
  },
582
636
  },
583
637
 
638
+ {
639
+ name: 'scraper_verify_token',
640
+ description: "Show the calling site's web-scraper verification header (name + value), the scraper IP, and ready-to-send instructions for allowlisting Velaro's crawler with the customer's web host/WAF. Wraps GET ScraperJobs/VerifyToken. Site comes from auth context, never a parameter.",
641
+ inputSchema: { type: 'object', properties: {} },
642
+ },
643
+
584
644
  // ── Startup migration diagnostics (AdminStartupDiagnostics canary table) ──
585
645
  {
586
646
  name: 'admin_startup_diagnostics',
@@ -2962,6 +3022,21 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2962
3022
  required: [],
2963
3023
  },
2964
3024
  },
3025
+ {
3026
+ name: 'ai_spend_report',
3027
+ description: 'SuperAdmin AI spend report: logged AI cost for prod and/or staging over a window, by site, model, key source (platform/copilot/byok) and site x feature, with cache-read ratio, QA share, top movers vs the previous period and anomalies. Also records/lists provider reconciliation (console total vs logged). Breaker trips are not in AiCostLogs so are reported as not available.',
3028
+ inputSchema: {
3029
+ type: 'object',
3030
+ properties: {
3031
+ env: { type: 'string', enum: ['both', 'prod', 'staging'], description: 'Environment. Default both.' },
3032
+ days: { type: 'number', description: 'Trailing window in days. Default 7, max 92.' },
3033
+ reconcile_entered_usd: { type: 'number', description: 'If set, records a reconciliation for the window with this provider console total in USD instead of only reading the report.' },
3034
+ provider: { type: 'string', description: 'Provider for reconciliation. Default Anthropic.' },
3035
+ key_bucket: { type: 'string', description: 'Per-key bucket for reconciliation: platform, copilot or byok. Omit for provider total.' },
3036
+ },
3037
+ required: [],
3038
+ },
3039
+ },
2965
3040
  {
2966
3041
  name: 'signalr_diagnostics',
2967
3042
  description: 'Check health, connection counts, hub stats, and recent errors for Velaro\'s Azure SignalR Service instances. Covers both the infra-prod instance (velaro-eastus-prd-signalr, Premium P1) and the legacy prod instance (velaro-prod-us). Use this to diagnose "hub down", connection storms, capacity issues, or capacity saturation alerts.',
@@ -4756,6 +4831,11 @@ async function handleTool(name, args) {
4756
4831
  return JSON.stringify(result, null, 2);
4757
4832
  }
4758
4833
 
4834
+ case 'scraper_verify_token': {
4835
+ const result = await api('GET', 'ScraperJobs/VerifyToken');
4836
+ return JSON.stringify(result, null, 2);
4837
+ }
4838
+
4759
4839
  case 'site_scraper_usage': {
4760
4840
  const result = await api('GET', `ScraperJobs/StaffUsage/${encodeURIComponent(args.siteId)}`);
4761
4841
  return JSON.stringify(result, null, 2);
@@ -7352,6 +7432,22 @@ async function handleTool(name, args) {
7352
7432
  case 'velaro_diagnostic_start':
7353
7433
  return DIAGNOSTIC_RUNBOOK;
7354
7434
 
7435
+ case 'ai_spend_report': {
7436
+ const days = Math.min(Math.max(Number(args.days) || 7, 1), 92);
7437
+ const to = new Date();
7438
+ const from = new Date(to.getTime() - days * 86400000);
7439
+ const env = args.env || 'both';
7440
+ if (args.reconcile_entered_usd !== undefined && args.reconcile_entered_usd !== null) {
7441
+ const row = await api('POST', '/SuperAdmin/AiSpend/Reconciliations', {
7442
+ periodStartUtc: from.toISOString(), periodEndUtc: to.toISOString(), environment: env,
7443
+ provider: args.provider || 'Anthropic', keyLabel: args.key_bucket || null, enteredUsd: Number(args.reconcile_entered_usd),
7444
+ });
7445
+ return JSON.stringify(row, null, 2);
7446
+ }
7447
+ const data = await api('GET', `/SuperAdmin/AiSpend/Report?env=${env}&from=${encodeURIComponent(from.toISOString())}&to=${encodeURIComponent(to.toISOString())}`);
7448
+ return JSON.stringify(data, null, 2);
7449
+ }
7450
+
7355
7451
  case 'signalr_diagnostics': {
7356
7452
  // Parse a SignalR connection string into { endpoint, accessKey }
7357
7453
  function parseSignalRConn(conn) {
@@ -8207,7 +8303,7 @@ async function handleTool(name, args) {
8207
8303
  }
8208
8304
 
8209
8305
  case 'billing_plan_status': {
8210
- const s = await api('GET', '/BillingPayment/plan-status');
8306
+ const s = await api('GET', '/BillingPayment/plan-status', undefined, MESSAGING_API);
8211
8307
  const lines = [`Site ${s.siteId} — Subscription Status`];
8212
8308
  if (s.subscription) {
8213
8309
  lines.push(`Seats: ${s.subscription.maxLicensedSeats} | Conv/mo: ${s.subscription.maxConversationsPerMonth || '∞'} | Self-serve: ${s.subscription.enableSelfServeSeats ? 'YES' : 'no'}`);