@velaro/mcp-server 0.1.0 → 0.4.0

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 (3) hide show
  1. package/README.md +230 -16
  2. package/package.json +32 -32
  3. package/server.js +666 -1
package/README.md CHANGED
@@ -1,20 +1,35 @@
1
1
  # @velaro/mcp-server
2
2
 
3
- Connect Claude, Claude Code, and other AI agents directly to your Velaro account via the [Model Context Protocol](https://modelcontextprotocol.io).
3
+ **The only customer support platform with a native, self-updating MCP server.**
4
4
 
5
- ## Quick Start
5
+ Connect Claude, Claude Code, Claude Desktop, or any MCP-compatible AI agent directly to your Velaro account — knowledge base, workflows, conversation data, and 596 AI skills across 20+ commerce and CRM platforms.
6
6
 
7
7
  ```bash
8
8
  npx @velaro/mcp-server
9
9
  ```
10
10
 
11
- Set your MCP key (create one in Velaro Settings > API > MCP Keys):
12
-
11
+ Set your key and go:
13
12
  ```bash
14
13
  VELARO_MCP_KEY=vel_live_... npx @velaro/mcp-server
15
14
  ```
16
15
 
17
- ## Claude Code Setup
16
+ ---
17
+
18
+ ## Why Velaro MCP Is Different
19
+
20
+ Most chat platforms require webhooks, custom API wrappers, or point-to-point integrations to give an AI agent access to customer data. Velaro ships a published npm package that turns Claude into a first-class operator of your entire support infrastructure — no wrapper code, no custom authentication layer, no waiting for an API integration to be built.
21
+
22
+ - **One command install** — `npx @velaro/mcp-server` pulls the latest version automatically
23
+ - **Hosted HTTP endpoint** — no Node.js required; point Claude Desktop at `https://velaro-admin-staging.azurewebsites.net/mcp` with a Bearer token
24
+ - **Auto-updates** — the package is republished on every production deploy; `npx` always runs the latest
25
+ - **596 AI skills** — Shopify, BigCommerce, WooCommerce, Magento, HubSpot, Salesforce, NetSuite, ServiceNow, Dynamics 365, SAP, Square, QuickBooks, Jobber, and more — all callable from within Claude
26
+ - **25 built-in tools** — conversations, workflows, bots, contacts, agents, teams, routing rules, conversion tracking, and full KB management
27
+
28
+ ---
29
+
30
+ ## Quick Start
31
+
32
+ ### Claude Code
18
33
 
19
34
  Add to `~/.claude/settings.json`:
20
35
 
@@ -32,9 +47,9 @@ Add to `~/.claude/settings.json`:
32
47
  }
33
48
  ```
34
49
 
35
- ## Claude Desktop Setup
50
+ ### Claude Desktop
36
51
 
37
- Add to `~/.claude/claude_desktop_config.json`:
52
+ Add to `~/.config/claude/claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
38
53
 
39
54
  ```json
40
55
  {
@@ -50,24 +65,223 @@ Add to `~/.claude/claude_desktop_config.json`:
50
65
  }
51
66
  ```
52
67
 
68
+ ### Hosted HTTP Endpoint (No Node Required)
69
+
70
+ If you prefer not to run a local Node process, point directly at Velaro's hosted MCP endpoint:
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "velaro": {
76
+ "url": "https://api-admin-us-east.velaro.com/mcp",
77
+ "headers": {
78
+ "Authorization": "Bearer vel_live_..."
79
+ }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ This works with any MCP client that supports HTTP/SSE transport — including Claude Desktop 0.8+.
86
+
87
+ ---
88
+
53
89
  ## Available Tools
54
90
 
55
- | Tool | Description |
91
+ ### Knowledge Base
92
+ | Tool | What It Does |
56
93
  |------|-------------|
57
- | `kb_list_topics` | List all KB topics |
58
- | `kb_search_articles` | Search articles by term, topic, or publish status |
59
- | `kb_get_article` | Get full article content by ID |
60
- | `kb_create_article` | Create a new article |
61
- | `kb_update_article` | Update an existing article |
94
+ | `kb_list_topics` | List all knowledge base topics |
95
+ | `kb_search_articles` | Search articles by keyword, topic, or publish status |
96
+ | `kb_get_article` | Retrieve full article content by ID |
97
+ | `kb_create_article` | Create and optionally publish a new article |
98
+ | `kb_update_article` | Update title, body, tags, or publish status |
62
99
  | `kb_delete_article` | Delete an article |
63
100
 
64
- ## Auth
101
+ ### Conversations
102
+ | Tool | What It Does |
103
+ |------|-------------|
104
+ | `conversation_list` | List recent conversations filtered by channel, status, or agent |
105
+ | `conversation_get` | Get full details for a single conversation |
106
+ | `conversation_search` | Search conversations by phone number or call SID |
107
+
108
+ ### Workflows
109
+ | Tool | What It Does |
110
+ |------|-------------|
111
+ | `workflow_list` | List all workflows with enabled state and trigger type |
112
+ | `workflow_get` | Get full workflow details by ID |
113
+ | `workflow_toggle` | Enable or disable a workflow |
114
+
115
+ ### Bots & AI Configurations
116
+ | Tool | What It Does |
117
+ |------|-------------|
118
+ | `bot_list` | List all bot/AI configurations for this site |
119
+ | `bot_get` | Get full bot config including system prompt |
120
+ | `bot_update_prompt` | Update a bot's system prompt |
121
+
122
+ ### Contacts
123
+ | Tool | What It Does |
124
+ |------|-------------|
125
+ | `contact_search` | Search contacts by name, email, phone, or company |
126
+ | `contact_get` | Get full contact details by ID |
127
+
128
+ ### Team & Routing
129
+ | Tool | What It Does |
130
+ |------|-------------|
131
+ | `agent_list` | List agents with availability status; filter by team |
132
+ | `team_list` | List all teams for this site |
133
+ | `routing_list` | List all routing rules ordered by priority |
134
+ | `routing_get` | Get routing rule details including rule and value JSON |
135
+
136
+ ### Site
137
+ | Tool | What It Does |
138
+ |------|-------------|
139
+ | `site_info` | Get site settings, active channels, subscription features, and resource counts |
140
+
141
+ ### Live Operations
142
+ | Tool | What It Does |
143
+ |------|-------------|
144
+ | `kpi_snapshot` | Real-time ops dashboard — agents online/available/busy, open/queued conversations, bot-active count, resolved today, queue wait times, top agents, channel breakdown, per-team coverage status |
145
+
146
+ ### CSAT
147
+ | Tool | What It Does |
148
+ |------|-------------|
149
+ | `csat_summary` | Customer satisfaction — avg rating, satisfaction rate, star breakdown, recent verbatim comments |
150
+
151
+ ### Bot Diagnostics
152
+ | Tool | What It Does |
153
+ |------|-------------|
154
+ | `bot_analytics_summary` | AI bot health — total skill calls, error count, success rate, top skill, training count |
155
+ | `bot_skill_usage` | Per-skill call counts by integration + error counts — identifies over/underused skills and failing integrations |
156
+ | `bot_recent_errors` | Last N AI skill errors with integration, message, and timestamp — for active troubleshooting |
157
+
158
+ ### Service Level
159
+ | Tool | What It Does |
160
+ |------|-------------|
161
+ | `service_level_report` | SLA compliance per period — % within threshold, avg response time, avg handle time. Granularity: hour/day/week/month |
162
+
163
+ ### Reporting & Analytics
164
+ | Tool | What It Does |
165
+ |------|-------------|
166
+ | `report_roi_summary` | Full ROI report — bot deflections, hours saved, cost savings, AI skill executions, resolution rate, response time, trends vs prior period, monthly breakdown, top skills by call count, channel breakdown |
167
+ | `report_agent_utilization` | Per-agent stats — conversation count, avg handle time, active/away breakdown for any date range |
168
+ | `report_campaign_performance` | Email campaign KPIs — open rate, click rate, delivery rate, complaint rate per campaign + aggregate totals |
169
+ | `report_campaign_deliverability` | Deliverability health check — bounce rate, complaint rate, unsubscribe rate, open rate with traffic-light status and fix recommendations |
170
+ | `report_campaign_monthly` | Month-by-month email volume for the last 12 months |
171
+ | `report_audience_growth` | Total active contacts, net contacts (minus unsubscribes), month-by-month additions |
172
+
173
+ ### Conversion Tracking
174
+ | Tool | What It Does |
175
+ |------|-------------|
176
+ | `conversion_list_goals` | List all conversion goals |
177
+ | `conversion_create_goal` | Create a new conversion goal (URL, Shopify, Square, custom event) |
178
+ | `conversion_get_report` | Get conversion attribution report — revenue, rates, timeline |
179
+
180
+ More tools are added on every production release. Run `tools/list` to see the current full list from your Claude client.
181
+
182
+ ---
183
+
184
+ ## Getting Your API Key
185
+
186
+ 1. Log in to your Velaro admin at [admin.velaro.com](https://admin.velaro.com)
187
+ 2. Go to **Settings → API → MCP Keys**
188
+ 3. Click **Create Key** → give it a label (e.g. "Claude Code")
189
+ 4. Copy the `vel_live_...` key — it's shown once
65
190
 
66
- - `VELARO_MCP_KEY=vel_live_...` service key (create in Settings > API > MCP Keys)
67
- - `VELARO_JWT=<token>` — short-lived JWT from `velaro login` (dev use only)
191
+ Keys are site-isolated: the key is bound to your Velaro account and can only read/write data for that account. You can create multiple keys and revoke them independently.
192
+
193
+ ---
194
+
195
+ ## Auth Options
196
+
197
+ | Variable | When to Use |
198
+ |----------|-------------|
199
+ | `VELARO_MCP_KEY=vel_live_...` | Production — create in Settings > API > MCP Keys |
200
+ | `VELARO_JWT=<token>` | Development only — short-lived JWT from `velaro login` |
201
+
202
+ ---
68
203
 
69
204
  ## Staging / Custom API
70
205
 
71
206
  ```bash
72
207
  VELARO_ADMIN_API=https://velaro-admin-staging.azurewebsites.net VELARO_MCP_KEY=vel_live_... npx @velaro/mcp-server
73
208
  ```
209
+
210
+ ---
211
+
212
+ ## HTTP Mode (Self-Hosted)
213
+
214
+ Run as an HTTP/SSE server instead of stdio — useful for shared deployments:
215
+
216
+ ```bash
217
+ VELARO_MCP_MODE=http PORT=3000 VELARO_MCP_KEY=vel_live_... node server.js
218
+ ```
219
+
220
+ Clients connect via `GET /mcp` (SSE stream) and `POST /mcp` (JSON-RPC 2.0).
221
+
222
+ ---
223
+
224
+ ## What You Can Do with Velaro + Claude
225
+
226
+ Once connected, Claude has direct operator access to your support infrastructure:
227
+
228
+ **Knowledge Base Management**
229
+ > "Search my KB for any articles about refund policy and update them to reflect the new 30-day window we announced last week."
230
+
231
+ **Workflow Diagnostics**
232
+ > "List all my disabled workflows and tell me which ones are AI-enhanced so I can decide which to turn back on."
233
+
234
+ **Bot Prompt Tuning**
235
+ > "Read the current system prompt for bot #3 and rewrite it to be more concise and focus on checkout support."
236
+
237
+ **Live Operations**
238
+ > "Show me the last 20 open conversations on the SMS channel and tell me which agents have the most active chats right now."
239
+
240
+ **Contact Lookup**
241
+ > "Search for the contact with email orders@example.com and show me their full profile."
242
+
243
+ **Routing Audit**
244
+ > "List all routing rules in priority order — I want to review what fires before the 'Default Team' fallback."
245
+
246
+ **Conversion Tracking**
247
+ > "Create a conversion goal that fires when a customer hits /order-confirmed, with a 24-hour attribution window and $50 default value."
248
+
249
+ **ROI & Analytics**
250
+ > "Pull the last 30-day ROI report. How many conversations did the bot deflect, how many hours did that save, and which AI skills were called the most?"
251
+
252
+ **Deliverability Monitoring**
253
+ > "Check our email deliverability health — is our bounce rate or complaint rate in the red? What should we fix?"
254
+
255
+ **Site Health Check**
256
+ > "Give me a full site overview — active channels, agent count, workflow status, and which subscription features are enabled."
257
+
258
+ Claude handles these operations natively. No exports, no copy-paste, no waiting for a developer.
259
+
260
+ ---
261
+
262
+ ## How It's Updated
263
+
264
+ This package is automatically published to npm on every Velaro production deploy. When Velaro ships new tools or improves existing ones, your next `npx` invocation pulls the latest version automatically. You don't need to update a package.json or run `npm install`.
265
+
266
+ If you want a pinned version for reproducible environments:
267
+ ```bash
268
+ npx @velaro/mcp-server@0.1.0
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Competitive Context
274
+
275
+ Intercom, Zendesk, and Freshdesk do not publish MCP servers. Connecting Claude to those platforms requires building a custom MCP wrapper against their REST APIs, handling authentication yourself, and maintaining the integration as their APIs change.
276
+
277
+ Velaro ships the MCP server, hosts it, auto-updates it, and publishes it to npm. The integration is one `npx` command.
278
+
279
+ ---
280
+
281
+ ## Links
282
+
283
+ - [Velaro Admin](https://admin.velaro.com)
284
+ - [Documentation](https://help.velaro.com)
285
+ - [npm package](https://www.npmjs.com/package/@velaro/mcp-server)
286
+ - [Source](https://github.com/velaro/velaro-admin/tree/master/mcp)
287
+ - [MCP Specification](https://modelcontextprotocol.io)
package/package.json CHANGED
@@ -1,32 +1,32 @@
1
- {
2
- "name": "@velaro/mcp-server",
3
- "version": "0.1.0",
4
- "description": "Velaro MCP server — connect Claude and other AI agents directly to your Velaro KB and account.",
5
- "type": "module",
6
- "bin": {
7
- "velaro-mcp": "server.js"
8
- },
9
- "engines": {
10
- "node": ">=18.0.0"
11
- },
12
- "dependencies": {
13
- "@modelcontextprotocol/sdk": "^1.0.0"
14
- },
15
- "scripts": {
16
- "start": "node server.js"
17
- },
18
- "publishConfig": {
19
- "access": "public",
20
- "registry": "https://registry.npmjs.org/"
21
- },
22
- "repository": {
23
- "type": "git",
24
- "url": "https://github.com/velaro/velaro-admin"
25
- },
26
- "keywords": ["velaro", "mcp", "knowledge-base", "claude", "ai", "model-context-protocol"],
27
- "license": "MIT",
28
- "files": [
29
- "server.js",
30
- "README.md"
31
- ]
32
- }
1
+ {
2
+ "name": "@velaro/mcp-server",
3
+ "version": "0.4.0",
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
+ "type": "module",
6
+ "bin": {
7
+ "velaro-mcp": "server.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18.0.0"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.0.0"
14
+ },
15
+ "scripts": {
16
+ "start": "node server.js"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/velaro/velaro-admin"
25
+ },
26
+ "keywords": ["velaro", "mcp", "knowledge-base", "claude", "ai", "model-context-protocol", "customer-support", "workflows", "conversations"],
27
+ "license": "MIT",
28
+ "files": [
29
+ "server.js",
30
+ "README.md"
31
+ ]
32
+ }
package/server.js CHANGED
@@ -74,6 +74,30 @@ async function api(method, path, body) {
74
74
  return text ? JSON.parse(text) : null;
75
75
  }
76
76
 
77
+ // Call the hosted MCP gateway (JSON-RPC) for tools that are dispatched server-side.
78
+ async function rpc(toolName, args) {
79
+ const body = {
80
+ jsonrpc: '2.0',
81
+ id: 1,
82
+ method: 'tools/call',
83
+ params: { name: toolName, arguments: args ?? {} },
84
+ };
85
+ const res = await fetch(`${API_BASE}/mcp`, {
86
+ method: 'POST',
87
+ headers: { Authorization: authHeader(), 'Content-Type': 'application/json' },
88
+ body: JSON.stringify(body),
89
+ });
90
+ if (!res.ok) {
91
+ const text = await res.text().catch(() => '');
92
+ throw new Error(`Velaro MCP gateway ${toolName} -> ${res.status}: ${text.slice(0, 300)}`);
93
+ }
94
+ const data = await res.json();
95
+ if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
96
+ const content = data.result?.content?.[0]?.text;
97
+ if (content === undefined) throw new Error('Empty response from MCP gateway');
98
+ return content;
99
+ }
100
+
77
101
  // ── Tool definitions ──────────────────────────────────────────────────────────
78
102
 
79
103
  const TOOLS = [
@@ -149,6 +173,324 @@ const TOOLS = [
149
173
  required: ['id'],
150
174
  },
151
175
  },
176
+
177
+ // ── Conversations ─────────────────────────────────────────────────────────
178
+ {
179
+ name: 'conversation_list',
180
+ description: 'List recent conversations. Returns id, status, phoneNumber, assignedUserId, teamId, createdAt.',
181
+ inputSchema: {
182
+ type: 'object',
183
+ properties: {
184
+ limit: { type: 'number', description: 'Max results (default 20, max 50)' },
185
+ channel: { type: 'string', description: 'Filter by channel (e.g. web, sms, ivr)' },
186
+ status: { type: 'string', description: 'Filter by status: open or closed' },
187
+ agentId: { type: 'number', description: 'Filter by assigned agent ID' },
188
+ },
189
+ },
190
+ },
191
+ {
192
+ name: 'conversation_get',
193
+ description: 'Get details for a single conversation by ID.',
194
+ inputSchema: {
195
+ type: 'object',
196
+ properties: { id: { type: 'number', description: 'Conversation ID' } },
197
+ required: ['id'],
198
+ },
199
+ },
200
+ {
201
+ name: 'conversation_search',
202
+ description: 'Search conversations by phone number or call SID.',
203
+ inputSchema: {
204
+ type: 'object',
205
+ properties: {
206
+ q: { type: 'string', description: 'Phone number or call SID to search' },
207
+ limit: { type: 'number', description: 'Max results (default 10, max 50)' },
208
+ },
209
+ required: ['q'],
210
+ },
211
+ },
212
+
213
+ // ── Workflows ─────────────────────────────────────────────────────────────
214
+ {
215
+ name: 'workflow_list',
216
+ description: 'List all workflows. Returns id, name, enabled, triggerType, AIEnhanced, category.',
217
+ inputSchema: {
218
+ type: 'object',
219
+ properties: {
220
+ enabled: { type: 'boolean', description: 'Filter by enabled state (omit for all)' },
221
+ },
222
+ },
223
+ },
224
+ {
225
+ name: 'workflow_get',
226
+ description: 'Get workflow details by ID.',
227
+ inputSchema: {
228
+ type: 'object',
229
+ properties: { id: { type: 'number', description: 'Workflow ID' } },
230
+ required: ['id'],
231
+ },
232
+ },
233
+ {
234
+ name: 'workflow_toggle',
235
+ description: 'Enable or disable a workflow by ID.',
236
+ inputSchema: {
237
+ type: 'object',
238
+ properties: {
239
+ id: { type: 'number', description: 'Workflow ID' },
240
+ enabled: { type: 'boolean', description: 'true to enable, false to disable' },
241
+ },
242
+ required: ['id', 'enabled'],
243
+ },
244
+ },
245
+
246
+ // ── Bots / AI Configurations ──────────────────────────────────────────────
247
+ {
248
+ name: 'bot_list',
249
+ description: 'List all bot/AI configurations for this site.',
250
+ inputSchema: { type: 'object', properties: {}, required: [] },
251
+ },
252
+ {
253
+ name: 'bot_get',
254
+ description: 'Get full bot configuration including system prompt.',
255
+ inputSchema: {
256
+ type: 'object',
257
+ properties: { id: { type: 'number', description: 'Bot configuration ID' } },
258
+ required: ['id'],
259
+ },
260
+ },
261
+ {
262
+ name: 'bot_update_prompt',
263
+ description: 'Update a bot\'s system prompt. Use bot_get first to read the current prompt.',
264
+ inputSchema: {
265
+ type: 'object',
266
+ properties: {
267
+ id: { type: 'number', description: 'Bot configuration ID' },
268
+ prompt: { type: 'string', description: 'New system prompt text' },
269
+ },
270
+ required: ['id', 'prompt'],
271
+ },
272
+ },
273
+
274
+ // ── Contacts ──────────────────────────────────────────────────────────────
275
+ {
276
+ name: 'contact_search',
277
+ description: 'Search contacts by name, email, phone, or company.',
278
+ inputSchema: {
279
+ type: 'object',
280
+ properties: {
281
+ q: { type: 'string', description: 'Name, email, phone, or company to search' },
282
+ limit: { type: 'number', description: 'Max results (default 10, max 50)' },
283
+ },
284
+ required: ['q'],
285
+ },
286
+ },
287
+ {
288
+ name: 'contact_get',
289
+ description: 'Get full contact details by ID.',
290
+ inputSchema: {
291
+ type: 'object',
292
+ properties: { id: { type: 'number', description: 'Contact ID' } },
293
+ required: ['id'],
294
+ },
295
+ },
296
+
297
+ // ── Agents ────────────────────────────────────────────────────────────────
298
+ {
299
+ name: 'agent_list',
300
+ description: 'List agents with their status (Available, Busy, Away, Offline). Optionally filter by team.',
301
+ inputSchema: {
302
+ type: 'object',
303
+ properties: {
304
+ teamId: { type: 'number', description: 'Filter by team ID (omit for all agents)' },
305
+ },
306
+ },
307
+ },
308
+
309
+ // ── Teams ─────────────────────────────────────────────────────────────────
310
+ {
311
+ name: 'team_list',
312
+ description: 'List all teams for this site.',
313
+ inputSchema: { type: 'object', properties: {}, required: [] },
314
+ },
315
+
316
+ // ── Routing Rules ─────────────────────────────────────────────────────────
317
+ {
318
+ name: 'routing_list',
319
+ description: 'List all routing rules ordered by priority.',
320
+ inputSchema: { type: 'object', properties: {}, required: [] },
321
+ },
322
+ {
323
+ name: 'routing_get',
324
+ description: 'Get routing rule details including rule and value JSON.',
325
+ inputSchema: {
326
+ type: 'object',
327
+ properties: { id: { type: 'number', description: 'Routing rule ID' } },
328
+ required: ['id'],
329
+ },
330
+ },
331
+
332
+ // ── Site ──────────────────────────────────────────────────────────────────
333
+ {
334
+ name: 'site_info',
335
+ description: 'Get site settings, active channels, subscription features, and counts of agents, teams, bots, and active workflows.',
336
+ inputSchema: { type: 'object', properties: {}, required: [] },
337
+ },
338
+
339
+ // ── Live Operations ───────────────────────────────────────────────────────
340
+ {
341
+ name: 'kpi_snapshot',
342
+ description: 'Real-time operations snapshot: agents online/available/busy, open/queued conversations, bot-active count, resolved today, missed/abandoned today, avg and longest queue wait, avg handle time, top agents by resolved count, channel breakdown, and per-team detail (coverage status, agent availability, unassigned conversations). Perfect for a live ops health check.',
343
+ inputSchema: { type: 'object', properties: {}, required: [] },
344
+ },
345
+
346
+ // ── CSAT ──────────────────────────────────────────────────────────────────
347
+ {
348
+ name: 'csat_summary',
349
+ description: 'Customer satisfaction (CSAT) report — avg rating, satisfaction rate (% rated 4-5 stars), breakdown by star rating, and up to 10 recent verbatim comments with channel and date. Use to gauge customer happiness and spot qualitative feedback trends.',
350
+ inputSchema: {
351
+ type: 'object',
352
+ properties: {
353
+ days: { type: 'number', description: 'Lookback window in days (1–365). Default: 30' },
354
+ },
355
+ },
356
+ },
357
+
358
+ // ── Bot Diagnostics ───────────────────────────────────────────────────────
359
+ {
360
+ name: 'bot_analytics_summary',
361
+ description: 'AI bot health summary — total AI skill calls, error count, success rate %, top skill name and call count, training conversations indexed. Quick pulse check on bot reliability.',
362
+ inputSchema: {
363
+ type: 'object',
364
+ properties: {
365
+ days: { type: 'number', description: 'Lookback window in days (1–365). Default: 30' },
366
+ },
367
+ },
368
+ },
369
+ {
370
+ name: 'bot_skill_usage',
371
+ description: 'Per-skill call counts broken down by integration — shows which AI tools are being used, how often, and error counts per integration. Use to identify overused or underused skills and diagnose high-error integrations.',
372
+ inputSchema: {
373
+ type: 'object',
374
+ properties: {
375
+ days: { type: 'number', description: 'Lookback window in days (1–365). Default: 30' },
376
+ },
377
+ },
378
+ },
379
+ {
380
+ name: 'bot_recent_errors',
381
+ description: 'Recent AI skill errors — returns the last N error log entries with integration name, error message, and timestamp. Use to diagnose why a specific integration or skill is failing.',
382
+ inputSchema: {
383
+ type: 'object',
384
+ properties: {
385
+ days: { type: 'number', description: 'Lookback window in days (1–90). Default: 7' },
386
+ limit: { type: 'number', description: 'Max errors to return (1–100). Default: 20' },
387
+ },
388
+ },
389
+ },
390
+
391
+ // ── Service Level ─────────────────────────────────────────────────────────
392
+ {
393
+ name: 'service_level_report',
394
+ description: 'SLA / service level report — per-period rows showing conversation volume, % answered within SLA threshold, avg response time, avg handle time. Granularity: hour, day, week, or month.',
395
+ inputSchema: {
396
+ type: 'object',
397
+ properties: {
398
+ startDate: { type: 'string', description: 'ISO 8601 start date, e.g. 2026-01-01 (default: 30 days ago)' },
399
+ endDate: { type: 'string', description: 'ISO 8601 end date, e.g. 2026-04-19 (default: now)' },
400
+ granularity: { type: 'string', enum: ['hour', 'day', 'week', 'month'], description: 'Bucketing period. Default: day' },
401
+ },
402
+ },
403
+ },
404
+
405
+ // ── Reporting ─────────────────────────────────────────────────────────────
406
+ {
407
+ name: 'report_roi_summary',
408
+ description: 'ROI / Value Report — bot deflections, hours saved, estimated cost savings, AI skill executions, resolution rate, avg first-response time, trends vs prior period, monthly breakdown, top skills, channel breakdown. No PII returned.',
409
+ inputSchema: {
410
+ type: 'object',
411
+ properties: {
412
+ days: { type: 'number', description: 'Lookback window in days (1–365). Default: 30' },
413
+ agentCostPerHour: { type: 'number', description: 'Agent hourly cost in USD for savings estimate. Default: 25' },
414
+ avgHandleMinutes: { type: 'number', description: 'Average handle time per conversation in minutes. Default: 8' },
415
+ },
416
+ },
417
+ },
418
+ {
419
+ name: 'report_agent_utilization',
420
+ description: 'Agent utilization report — per-agent conversation counts, handle times, and availability breakdown for a date range.',
421
+ inputSchema: {
422
+ type: 'object',
423
+ properties: {
424
+ startDate: { type: 'string', description: 'ISO 8601 start date, e.g. 2026-01-01 (default: 30 days ago)' },
425
+ endDate: { type: 'string', description: 'ISO 8601 end date, e.g. 2026-04-19 (default: now)' },
426
+ },
427
+ },
428
+ },
429
+ {
430
+ name: 'report_campaign_performance',
431
+ description: 'Email campaign performance: open rate, click rate, delivery rate, complaint rate per campaign + aggregate KPIs.',
432
+ inputSchema: {
433
+ type: 'object',
434
+ properties: {
435
+ days: { type: 'number', description: 'Lookback window in days. Default: 90' },
436
+ },
437
+ },
438
+ },
439
+ {
440
+ name: 'report_campaign_deliverability',
441
+ description: 'Deliverability health check — bounce rate, complaint rate, unsubscribe rate, open rate. Each metric has a traffic-light status (green/yellow/red) and actionable recommendations.',
442
+ inputSchema: {
443
+ type: 'object',
444
+ properties: {
445
+ days: { type: 'number', description: 'Lookback window in days. Default: 30' },
446
+ },
447
+ },
448
+ },
449
+ {
450
+ name: 'report_campaign_monthly',
451
+ description: 'Monthly email volume trend — last 12 months of sent, opened, clicked counts for charting or analysis.',
452
+ inputSchema: { type: 'object', properties: {}, required: [] },
453
+ },
454
+ {
455
+ name: 'report_audience_growth',
456
+ description: 'Audience growth — total active contacts, unsubscribes, net contacts, and month-by-month contact additions for the last 12 months.',
457
+ inputSchema: { type: 'object', properties: {}, required: [] },
458
+ },
459
+
460
+ // ── Conversion tracking tools ─────────────────────────────────────────────
461
+ {
462
+ name: 'conversion_list_goals',
463
+ description: 'List all conversion goals for this site. Returns id, name, triggerType, defaultAmount, attributionWindowHours, isEnabled.',
464
+ inputSchema: { type: 'object', properties: {}, required: [] },
465
+ },
466
+ {
467
+ name: 'conversion_create_goal',
468
+ description: 'Create a new conversion goal. Use triggerType="url" for URL-based, "shopify_order"/"square_payment"/"bc_order"/"woo_order" for integration-native, "custom_event" for JS SDK.',
469
+ inputSchema: {
470
+ type: 'object',
471
+ properties: {
472
+ name: { type: 'string', description: 'Goal name, e.g. "Purchase Completed"' },
473
+ triggerType: { type: 'string', enum: ['url', 'shopify_order', 'square_payment', 'bc_order', 'woo_order', 'magento_order', 'custom_event'], description: 'What triggers this conversion' },
474
+ triggerUrl: { type: 'string', description: 'Conversion URL (for triggerType=url)' },
475
+ eventName: { type: 'string', description: 'Custom event name (for triggerType=custom_event)' },
476
+ defaultAmount: { type: 'number', description: 'Default revenue amount in USD (default: 0)' },
477
+ revenueVariableName: { type: 'string', description: 'JS variable name to read actual order amount from' },
478
+ allowOnce: { type: 'boolean', description: 'Count each visitor only once (default: false)' },
479
+ attributionWindowHours: { type: 'number', description: 'Hours after chat to attribute conversion: 1, 24, 168 (7d), 720 (30d). Default: 24' },
480
+ },
481
+ required: ['name', 'triggerType'],
482
+ },
483
+ },
484
+ {
485
+ name: 'conversion_get_report',
486
+ description: 'Get conversion attribution report: total revenue, attributed revenue, attribution rate, conversions by goal, timeline. Shows how chat drives sales.',
487
+ inputSchema: {
488
+ type: 'object',
489
+ properties: {
490
+ days: { type: 'number', description: 'Lookback window in days (7, 30, or 90). Default: 30' },
491
+ },
492
+ },
493
+ },
152
494
  ];
153
495
 
154
496
  // ── Tool handlers ─────────────────────────────────────────────────────────────
@@ -237,6 +579,329 @@ async function handleTool(name, args) {
237
579
  return `Article ${args.id} deleted.`;
238
580
  }
239
581
 
582
+ // ── Conversations ──────────────────────────────────────────────────────
583
+ case 'conversation_list':
584
+ return await rpc('conversation_list', args);
585
+
586
+ case 'conversation_get':
587
+ return await rpc('conversation_get', args);
588
+
589
+ case 'conversation_search':
590
+ return await rpc('conversation_search', args);
591
+
592
+ // ── Workflows ──────────────────────────────────────────────────────────
593
+ case 'workflow_list':
594
+ return await rpc('workflow_list', args);
595
+
596
+ case 'workflow_get':
597
+ return await rpc('workflow_get', args);
598
+
599
+ case 'workflow_toggle':
600
+ return await rpc('workflow_toggle', args);
601
+
602
+ // ── Bots / AI Configurations ───────────────────────────────────────────
603
+ case 'bot_list':
604
+ return await rpc('bot_list', args);
605
+
606
+ case 'bot_get':
607
+ return await rpc('bot_get', args);
608
+
609
+ case 'bot_update_prompt':
610
+ return await rpc('bot_update_prompt', args);
611
+
612
+ // ── Contacts ───────────────────────────────────────────────────────────
613
+ case 'contact_search':
614
+ return await rpc('contact_search', args);
615
+
616
+ case 'contact_get':
617
+ return await rpc('contact_get', args);
618
+
619
+ // ── Agents ─────────────────────────────────────────────────────────────
620
+ case 'agent_list':
621
+ return await rpc('agent_list', args);
622
+
623
+ // ── Teams ──────────────────────────────────────────────────────────────
624
+ case 'team_list':
625
+ return await rpc('team_list', args);
626
+
627
+ // ── Routing Rules ──────────────────────────────────────────────────────
628
+ case 'routing_list':
629
+ return await rpc('routing_list', args);
630
+
631
+ case 'routing_get':
632
+ return await rpc('routing_get', args);
633
+
634
+ // ── Site ───────────────────────────────────────────────────────────────
635
+ case 'site_info':
636
+ return await rpc('site_info', args);
637
+
638
+ case 'conversion_list_goals': {
639
+ const goals = await api('GET', '/Conversions/goals');
640
+ if (!Array.isArray(goals) || !goals.length) return 'No conversion goals configured.';
641
+ return goals.map(g =>
642
+ `[${g.id}] "${g.name}" | type: ${g.triggerType} | window: ${g.attributionWindowHours}h | default: $${g.defaultAmount} | ${g.isEnabled ? 'enabled' : 'paused'}`
643
+ ).join('\n');
644
+ }
645
+
646
+ case 'conversion_create_goal': {
647
+ const res = await api('POST', '/Conversions/goals', {
648
+ name: args.name,
649
+ triggerType: args.triggerType,
650
+ triggerUrl: args.triggerUrl || null,
651
+ eventName: args.eventName || null,
652
+ defaultAmount: args.defaultAmount ?? 0,
653
+ revenueVariableName: args.revenueVariableName || null,
654
+ allowOnce: args.allowOnce ?? false,
655
+ attributionWindowHours: args.attributionWindowHours ?? 24,
656
+ isEnabled: true,
657
+ });
658
+ return `Created conversion goal "${args.name}" with ID ${res.id}.`;
659
+ }
660
+
661
+ case 'conversion_get_report': {
662
+ const days = args.days ?? 30;
663
+ const r = await api('GET', `/Conversions/report?days=${days}`);
664
+ if (!r.hasData) {
665
+ return `No conversion data in the last ${days} days. ${r.goalCount} goal(s) configured — events will appear once the widget fires.`;
666
+ }
667
+ const s = r.summary;
668
+ const lines = [
669
+ `=== Conversion Attribution (last ${days} days) ===`,
670
+ `Attributed Revenue: $${s.attributedRevenue.toFixed(2)} of $${s.totalRevenue.toFixed(2)} total`,
671
+ `Attribution Rate: ${(s.attributionRate * 100).toFixed(1)}% (${s.attributedConversions} of ${s.totalConversions} conversions had a recent chat)`,
672
+ `Avg Order Value: $${s.avgOrderValue.toFixed(2)}`,
673
+ '',
674
+ '--- By Goal ---',
675
+ ...r.byGoal.map(g =>
676
+ ` ${g.goalName}: ${g.conversions} conversions, $${g.revenue.toFixed(2)} revenue, ${(g.attributionRate * 100).toFixed(1)}% attributed`
677
+ ),
678
+ ];
679
+ return lines.join('\n');
680
+ }
681
+
682
+ // ── Live Operations ───────────────────────────────────────────────────
683
+ case 'kpi_snapshot': {
684
+ const r = await api('GET', '/LiveDashboard/snapshot');
685
+ const k = r.kpis ?? {};
686
+ const lines = [
687
+ '=== Live Operations Snapshot ===',
688
+ `Agents: ${k.agentsAvailable} available, ${k.agentsBusy} busy, ${k.agentsOnline} online`,
689
+ `Conversations: ${k.openConversations} open | ${k.queuedConversations} queued | ${k.botActive} bot-active`,
690
+ `Today: ${k.resolvedToday} resolved | ${k.missedAbandonedToday} missed/abandoned`,
691
+ `Queue Wait: avg ${k.avgWaitMinutes}m | longest ${k.longestWaitMinutes}m`,
692
+ `Avg Handle Time: ${k.avgHandleTimeMinutes}m | Avg First Response: ${k.avgFirstResponseMinutes}m`,
693
+ '',
694
+ '--- Top Agents (resolved today) ---',
695
+ ...(r.topAgents?.length
696
+ ? r.topAgents.map(a => ` ${a.name}: ${a.resolved} resolved, ${a.active} active`)
697
+ : [' (no data yet today)']),
698
+ '',
699
+ '--- By Channel ---',
700
+ ...(r.channelBreakdown?.length
701
+ ? r.channelBreakdown.map(c => ` ${c.channel}: ${c.count} open, ${c.queued} queued`)
702
+ : [' (no data)']),
703
+ '',
704
+ '--- By Team ---',
705
+ ...(r.teams?.length
706
+ ? r.teams.map(t =>
707
+ ` ${t.teamName}: ${t.agentsAvailable}/${t.agentsOnline} available | ${t.openConversations} open | ${t.unassignedConversations} unassigned | wait avg ${t.avgWaitMinutes}m | ${t.coverageStatus}`
708
+ )
709
+ : [' (no teams)']),
710
+ ];
711
+ return lines.join('\n');
712
+ }
713
+
714
+ // ── CSAT ──────────────────────────────────────────────────────────────
715
+ case 'csat_summary': {
716
+ const days = args.days ?? 30;
717
+ const r = await api('GET', `/api/admin/Csat/Summary?days=${days}`);
718
+ if (!r.totalResponses) return `No CSAT responses in the last ${days} days.`;
719
+ const lines = [
720
+ `=== CSAT Summary (last ${days} days) ===`,
721
+ `Responses: ${r.totalResponses} | Avg Rating: ${r.averageRating}/5 | Satisfaction Rate: ${r.satisfactionRate}%`,
722
+ '',
723
+ '--- Rating Breakdown ---',
724
+ ...(r.breakdown?.map(b => ` ${b.rating}★: ${b.count}`) ?? []),
725
+ '',
726
+ '--- Recent Comments ---',
727
+ ...(r.recentComments?.length
728
+ ? r.recentComments.map(c => ` [${c.Rating}★] ${c.Channel ?? 'web'} — "${c.Comment}"`)
729
+ : [' (no comments)']),
730
+ ];
731
+ return lines.join('\n');
732
+ }
733
+
734
+ // ── Bot Diagnostics ───────────────────────────────────────────────────
735
+ case 'bot_analytics_summary': {
736
+ const days = args.days ?? 30;
737
+ const r = await api('GET', `/BotAnalytics/Summary?days=${days}`);
738
+ return [
739
+ `=== Bot Analytics (last ${days} days) ===`,
740
+ `AI Skill Calls: ${r.totalSkillCalls} | Errors: ${r.errorCount} | Success Rate: ${r.successRate}%`,
741
+ `Top Skill: ${r.topSkill} (${r.topSkillCount} calls)`,
742
+ `Conversations Trained: ${r.conversationsTrainedCount}`,
743
+ ].join('\n');
744
+ }
745
+
746
+ case 'bot_skill_usage': {
747
+ const days = args.days ?? 30;
748
+ const r = await api('GET', `/BotAnalytics/SkillUsage?days=${days}`);
749
+ const lines = [
750
+ `=== Bot Skill Usage (last ${days} days) ===`,
751
+ `Total Successful Calls: ${r.totalSuccessfulCalls} | Total Errors: ${r.totalErrors}`,
752
+ '',
753
+ '--- Skill Calls ---',
754
+ ...(r.skillUsage?.length
755
+ ? r.skillUsage.slice(0, 30).map(s => ` [${s.integration}] ${s.toolName}: ${s.count}`)
756
+ : [' (no data)']),
757
+ '',
758
+ '--- Errors by Integration ---',
759
+ ...(r.errorsByIntegration?.length
760
+ ? r.errorsByIntegration.map(e => ` ${e.integration}: ${e.errorCount} errors`)
761
+ : [' (none)']),
762
+ ];
763
+ return lines.join('\n');
764
+ }
765
+
766
+ case 'bot_recent_errors': {
767
+ const days = args.days ?? 7;
768
+ const limit = args.limit ?? 20;
769
+ const errors = await api('GET', `/BotAnalytics/RecentErrors?days=${days}&limit=${limit}`);
770
+ if (!Array.isArray(errors) || !errors.length) return `No AI skill errors in the last ${days} days.`;
771
+ const lines = [
772
+ `=== Recent Bot Errors (last ${days} days, up to ${limit}) ===`,
773
+ ...errors.map(e => ` [${e.integration ?? 'unknown'}] ${new Date(e.createdAt).toISOString().slice(0, 16)} — ${e.message ?? e.errorMessage ?? JSON.stringify(e)}`),
774
+ ];
775
+ return lines.join('\n');
776
+ }
777
+
778
+ // ── Service Level ─────────────────────────────────────────────────────
779
+ case 'service_level_report': {
780
+ const qs = [
781
+ args.startDate ? `startDate=${args.startDate}` : '',
782
+ args.endDate ? `endDate=${args.endDate}` : '',
783
+ args.granularity ? `granularity=${args.granularity}` : '',
784
+ ].filter(Boolean).join('&');
785
+ const r = await api('GET', `/ServiceLevel${qs ? '?' + qs : ''}`);
786
+ const rows = r.rows ?? r;
787
+ if (!Array.isArray(rows) || !rows.length) return 'No service level data for this period.';
788
+ const lines = [
789
+ `=== Service Level Report ===`,
790
+ ...rows.map(row =>
791
+ ` ${row.period ?? row.label ?? row.date}: ${row.totalConversations ?? row.total ?? 0} convos | SLA ${row.slaPercent ?? row.withinSla ?? 0}% | avg response ${row.avgResponseSecs ?? row.avgFirstResponseSecs ?? 0}s | avg handle ${row.avgHandleTimeSecs ?? row.avgHandleSecs ?? 0}s`
792
+ ),
793
+ ];
794
+ return lines.join('\n');
795
+ }
796
+
797
+ // ── Reporting ─────────────────────────────────────────────────────────
798
+ case 'report_roi_summary': {
799
+ const days = args.days ?? 30;
800
+ const cost = args.agentCostPerHour ?? 25;
801
+ const handle = args.avgHandleMinutes ?? 8;
802
+ const r = await api('GET', `/RoiReport/Summary?days=${days}&agentCostPerHour=${cost}&avgHandleMinutes=${handle}`);
803
+ const h = r.headline;
804
+ const t = r.trends;
805
+ const lines = [
806
+ `=== ROI Summary (last ${days} days) ===`,
807
+ `Total Conversations: ${h.totalConversations} (${t.conversationVolumeChange > 0 ? '+' : ''}${t.conversationVolumeChange}% vs prior period)`,
808
+ `Bot Deflections: ${h.botDeflections} (${h.deflectionRate}% ${t.deflectionRateChange > 0 ? '▲' : '▼'}${Math.abs(t.deflectionRateChange)}% vs prior)`,
809
+ `Human-Handled: ${h.humanHandledConversations}`,
810
+ `Hours Saved: ${h.hoursSaved} | Estimated Savings: $${h.estimatedSavings}`,
811
+ `AI Skill Executions: ${h.aiSkillExecutions}`,
812
+ `Resolution Rate: ${h.resolutionRate}% (${t.resolutionRateChange > 0 ? '+' : ''}${t.resolutionRateChange}% vs prior)`,
813
+ `Avg First Response: ${Math.round(h.avgFirstResponseSecs)}s`,
814
+ '',
815
+ '--- Top AI Skills ---',
816
+ ...(r.topSkills?.length ? r.topSkills.map(s => ` ${s.skill}: ${s.calls} calls`) : [' (no data)']),
817
+ '',
818
+ '--- By Channel ---',
819
+ ...(r.channelBreakdown?.length ? r.channelBreakdown.map(c => ` ${c.channel}: ${c.conversations} convos, ${c.deflectionPct}% bot-deflected`) : [' (no data)']),
820
+ '',
821
+ '--- Monthly Trend (last 6 months) ---',
822
+ ...(r.monthlyBreakdown?.length ? r.monthlyBreakdown.map(m =>
823
+ ` ${m.label}: ${m.total} total, ${m.botHandled} bot-handled (${m.deflectionRate}%), ${m.resolved} resolved, avg response ${m.avgResponseSecs}s`
824
+ ) : [' (no data)']),
825
+ ];
826
+ return lines.join('\n');
827
+ }
828
+
829
+ case 'report_agent_utilization': {
830
+ const qs = [
831
+ args.startDate ? `startDate=${args.startDate}` : '',
832
+ args.endDate ? `endDate=${args.endDate}` : '',
833
+ ].filter(Boolean).join('&');
834
+ const rows = await api('GET', `/Reports/AgentUtilization${qs ? '?' + qs : ''}`);
835
+ if (!Array.isArray(rows) || !rows.length) return 'No agent utilization data for this period.';
836
+ const lines = [
837
+ `=== Agent Utilization (${rows.length} agents) ===`,
838
+ ...rows.map(a =>
839
+ ` ${a.agentName} — ${a.totalConversations} convos | avg handle: ${a.avgHandleTimeSecs}s | active: ${a.activePercent}% | away: ${a.awayPercent}%`
840
+ ),
841
+ ];
842
+ return lines.join('\n');
843
+ }
844
+
845
+ case 'report_campaign_performance': {
846
+ const days = args.days ?? 90;
847
+ const r = await api('GET', `/CampaignReports/Performance?days=${days}`);
848
+ const s = r.summary ?? {};
849
+ const lines = [
850
+ `=== Campaign Performance (last ${days} days, ${s.campaignCount ?? 0} campaigns) ===`,
851
+ `Total Sent: ${s.totalSent ?? 0} | Opens: ${s.totalOpened ?? 0} (${s.avgOpenRate ?? 0}%) | Clicks: ${s.totalClicked ?? 0} (${s.avgClickRate ?? 0}%)`,
852
+ '',
853
+ '--- Per Campaign ---',
854
+ ...(Array.isArray(r.campaigns) && r.campaigns.length
855
+ ? r.campaigns.map(c =>
856
+ ` [${c.Id}] "${c.Name}" (${c.Channel}) ${c.SentAt?.slice(0, 10)} — sent ${c.SentCount}, open ${c.openRate}%, click ${c.clickRate}%, bounce ${c.bounceRate ?? 0}%`
857
+ )
858
+ : [' (no campaigns sent in this window)']),
859
+ ];
860
+ return lines.join('\n');
861
+ }
862
+
863
+ case 'report_campaign_deliverability': {
864
+ const days = args.days ?? 30;
865
+ const r = await api('GET', `/CampaignReports/Deliverability?days=${days}`);
866
+ const lines = [
867
+ `=== Deliverability Health (${r.period}) ===`,
868
+ `Total Sent: ${r.totalSent}`,
869
+ `Delivery Rate: ${r.deliveryRate}% [${r.deliveryStatus?.toUpperCase()}]`,
870
+ `Bounce Rate: ${r.bounceRate}% [${r.bounceStatus?.toUpperCase()}] (hard: ${r.totalHardBounce}, soft: ${r.totalSoftBounce})`,
871
+ `Complaint Rate: ${r.complaintRate}% [${r.complaintStatus?.toUpperCase()}]`,
872
+ `Unsubscribe Rate: ${r.unsubRate}% [${r.unsubStatus?.toUpperCase()}] (${r.unsubCount} unsubs)`,
873
+ `Open Rate: ${r.openRate}% [${r.openStatus?.toUpperCase()}]`,
874
+ '',
875
+ '--- Recommendations ---',
876
+ ...(r.recommendations?.length ? r.recommendations.map(rec => ` • ${rec}`) : [' All metrics healthy.']),
877
+ ];
878
+ return lines.join('\n');
879
+ }
880
+
881
+ case 'report_campaign_monthly': {
882
+ const rows = await api('GET', '/CampaignReports/Monthly');
883
+ if (!Array.isArray(rows) || !rows.length) return 'No monthly campaign data (no sent campaigns in the last 12 months).';
884
+ const lines = [
885
+ '=== Monthly Email Volume (last 12 months) ===',
886
+ ...rows.map(m => ` ${m.month}: sent ${m.sent}, opened ${m.opened}, clicked ${m.clicked}`),
887
+ ];
888
+ return lines.join('\n');
889
+ }
890
+
891
+ case 'report_audience_growth': {
892
+ const r = await api('GET', '/CampaignReports/AudienceGrowth');
893
+ const lines = [
894
+ `=== Audience Growth ===`,
895
+ `Total Active Contacts: ${r.totalContacts} | Unsubscribed: ${r.unsubTotal} | Net: ${r.netContacts}`,
896
+ '',
897
+ '--- Monthly Additions (last 12 months) ---',
898
+ ...(r.monthly?.length
899
+ ? r.monthly.map(m => ` ${m.month}: +${m.added} contacts`)
900
+ : [' (no data)']),
901
+ ];
902
+ return lines.join('\n');
903
+ }
904
+
240
905
  default:
241
906
  throw new Error(`Unknown tool: ${name}`);
242
907
  }
@@ -245,7 +910,7 @@ async function handleTool(name, args) {
245
910
  // ── Server setup ──────────────────────────────────────────────────────────────
246
911
 
247
912
  const server = new Server(
248
- { name: 'velaro', version: '0.1.0' },
913
+ { name: 'velaro', version: '0.4.0' },
249
914
  { capabilities: { tools: {} } }
250
915
  );
251
916